@praxisui/charts 9.0.0-beta.72 → 9.0.0-beta.74
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +18 -1
- package/ai/component-registry.json +326 -36
- package/fesm2022/praxisui-charts.mjs +2390 -411
- package/package.json +3 -3
- package/types/praxisui-charts.d.ts +475 -85
|
@@ -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();
|
|
@@ -1439,7 +1587,10 @@ class PraxisChartComponent {
|
|
|
1439
1587
|
settingsPanel = inject(SETTINGS_PANEL_BRIDGE, { optional: true });
|
|
1440
1588
|
destroyRef = inject(DestroyRef);
|
|
1441
1589
|
resizeObserver = signal(null, ...(ngDevMode ? [{ debugName: "resizeObserver" }] : /* istanbul ignore next */ []));
|
|
1590
|
+
resizeObserverHost = null;
|
|
1442
1591
|
shellObserver = signal(null, ...(ngDevMode ? [{ debugName: "shellObserver" }] : /* istanbul ignore next */ []));
|
|
1592
|
+
themeObserver = signal(null, ...(ngDevMode ? [{ debugName: "themeObserver" }] : /* istanbul ignore next */ []));
|
|
1593
|
+
inheritedTheme = signal(null, ...(ngDevMode ? [{ debugName: "inheritedTheme" }] : /* istanbul ignore next */ []));
|
|
1443
1594
|
currentLoadState = signal('idle', ...(ngDevMode ? [{ debugName: "currentLoadState" }] : /* istanbul ignore next */ []));
|
|
1444
1595
|
remoteResolvedData = signal(null, ...(ngDevMode ? [{ debugName: "remoteResolvedData" }] : /* istanbul ignore next */ []));
|
|
1445
1596
|
remoteRuntimeState = signal('idle', ...(ngDevMode ? [{ debugName: "remoteRuntimeState" }] : /* istanbul ignore next */ []));
|
|
@@ -1455,11 +1606,14 @@ class PraxisChartComponent {
|
|
|
1455
1606
|
previousRemoteSignature = null;
|
|
1456
1607
|
previousRemoteDataResolver = undefined;
|
|
1457
1608
|
previousDocumentSignature = null;
|
|
1609
|
+
inheritedThemeSignature = null;
|
|
1458
1610
|
editorSessionSubscriptions = [];
|
|
1611
|
+
remoteRequestSubscription = null;
|
|
1612
|
+
remoteRequestSequence = 0;
|
|
1459
1613
|
effectiveConfig = computed(() => {
|
|
1460
|
-
const base = this.mappedRuntimeConfig() ?? this.config();
|
|
1461
|
-
const runtimeQueryContext =
|
|
1462
|
-
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);
|
|
1463
1617
|
if (!runtimeQueryContext
|
|
1464
1618
|
&& !Object.keys(runtimeFilters).length) {
|
|
1465
1619
|
return base;
|
|
@@ -1534,18 +1688,17 @@ class PraxisChartComponent {
|
|
|
1534
1688
|
};
|
|
1535
1689
|
}
|
|
1536
1690
|
if (remoteState === 'error') {
|
|
1691
|
+
const technicalDetails = this.remoteTechnicalError() ?? config.state?.error?.technicalDetails;
|
|
1537
1692
|
return {
|
|
1538
1693
|
...config,
|
|
1539
1694
|
preferredLoadState: 'error',
|
|
1540
|
-
state:
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
error
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
}
|
|
1548
|
-
: config.state,
|
|
1695
|
+
state: {
|
|
1696
|
+
...config.state,
|
|
1697
|
+
error: {
|
|
1698
|
+
...config.state?.error,
|
|
1699
|
+
technicalDetails,
|
|
1700
|
+
},
|
|
1701
|
+
},
|
|
1549
1702
|
};
|
|
1550
1703
|
}
|
|
1551
1704
|
if (remoteData !== null) {
|
|
@@ -1614,6 +1767,7 @@ class PraxisChartComponent {
|
|
|
1614
1767
|
constructor() {
|
|
1615
1768
|
afterNextRender(() => {
|
|
1616
1769
|
this.observeShellSizingContext();
|
|
1770
|
+
this.observeThemeContext();
|
|
1617
1771
|
});
|
|
1618
1772
|
effect(() => {
|
|
1619
1773
|
const document = this.chartDocument();
|
|
@@ -1646,8 +1800,9 @@ class PraxisChartComponent {
|
|
|
1646
1800
|
const config = this.effectiveConfig();
|
|
1647
1801
|
const explicitData = this.data();
|
|
1648
1802
|
const remoteDataResolver = this.remoteDataResolver();
|
|
1803
|
+
const queryContext = normalizePraxisDataQueryContext(this.queryContext());
|
|
1649
1804
|
const nextSignature = explicitData === null || explicitData === undefined
|
|
1650
|
-
? this.buildRemoteSignature(config)
|
|
1805
|
+
? this.buildRemoteSignature(config, queryContext)
|
|
1651
1806
|
: null;
|
|
1652
1807
|
if (nextSignature === this.previousRemoteSignature
|
|
1653
1808
|
&& remoteDataResolver === this.previousRemoteDataResolver) {
|
|
@@ -1655,6 +1810,7 @@ class PraxisChartComponent {
|
|
|
1655
1810
|
}
|
|
1656
1811
|
this.previousRemoteSignature = nextSignature;
|
|
1657
1812
|
this.previousRemoteDataResolver = remoteDataResolver;
|
|
1813
|
+
this.cancelRemoteDataRequest();
|
|
1658
1814
|
this.remoteResolvedData.set(null);
|
|
1659
1815
|
this.remoteRuntimeState.set('idle');
|
|
1660
1816
|
this.remoteTechnicalError.set(null);
|
|
@@ -1675,25 +1831,42 @@ class PraxisChartComponent {
|
|
|
1675
1831
|
if (this.remoteRuntimeState() === 'loading' || this.remoteResolvedData() !== null) {
|
|
1676
1832
|
return;
|
|
1677
1833
|
}
|
|
1834
|
+
const queryContext = normalizePraxisDataQueryContext(this.queryContext());
|
|
1678
1835
|
const event = {
|
|
1679
1836
|
chartId: effectiveConfig.id,
|
|
1680
1837
|
dataSource: effectiveConfig.dataSource,
|
|
1681
1838
|
query: effectiveConfig.dataSource.query,
|
|
1682
|
-
queryContext
|
|
1839
|
+
queryContext,
|
|
1683
1840
|
};
|
|
1684
1841
|
this.queryRequest.emit(event);
|
|
1842
|
+
const unsupportedExpression = resolveUnsupportedStatsFilterExpressionDiagnostic(queryContext);
|
|
1843
|
+
if (unsupportedExpression) {
|
|
1844
|
+
this.cancelRemoteDataRequest();
|
|
1845
|
+
this.remoteRuntimeState.set('error');
|
|
1846
|
+
this.remoteResolvedData.set([]);
|
|
1847
|
+
this.remoteTechnicalError.set(unsupportedExpression);
|
|
1848
|
+
return;
|
|
1849
|
+
}
|
|
1685
1850
|
this.remoteRuntimeState.set('loading');
|
|
1686
1851
|
this.remoteResolvedData.set([]);
|
|
1687
1852
|
this.remoteTechnicalError.set(null);
|
|
1688
|
-
this.
|
|
1853
|
+
this.cancelRemoteDataRequest();
|
|
1854
|
+
const requestId = ++this.remoteRequestSequence;
|
|
1855
|
+
this.remoteRequestSubscription = this.executeRemoteDataRequest(event, effectiveConfig)
|
|
1689
1856
|
.pipe(takeUntilDestroyed(this.destroyRef))
|
|
1690
1857
|
.subscribe({
|
|
1691
1858
|
next: (rows) => {
|
|
1859
|
+
if (requestId !== this.remoteRequestSequence) {
|
|
1860
|
+
return;
|
|
1861
|
+
}
|
|
1692
1862
|
this.remoteResolvedData.set(rows);
|
|
1693
1863
|
this.remoteRuntimeState.set('ready');
|
|
1694
1864
|
this.remoteTechnicalError.set(null);
|
|
1695
1865
|
},
|
|
1696
1866
|
error: (error) => {
|
|
1867
|
+
if (requestId !== this.remoteRequestSequence) {
|
|
1868
|
+
return;
|
|
1869
|
+
}
|
|
1697
1870
|
this.remoteResolvedData.set([]);
|
|
1698
1871
|
this.remoteRuntimeState.set('error');
|
|
1699
1872
|
this.remoteTechnicalError.set(error instanceof Error && error.message.trim()
|
|
@@ -1706,7 +1879,9 @@ class PraxisChartComponent {
|
|
|
1706
1879
|
this.renderAttempt();
|
|
1707
1880
|
if (this.loadState() !== 'ready') {
|
|
1708
1881
|
this.cancelScheduledRender();
|
|
1882
|
+
this.cancelScheduledResize();
|
|
1709
1883
|
this.deferredRenderAttempts = 0;
|
|
1884
|
+
this.destroyResizeObserver();
|
|
1710
1885
|
this.engine.destroy();
|
|
1711
1886
|
return;
|
|
1712
1887
|
}
|
|
@@ -1730,8 +1905,10 @@ class PraxisChartComponent {
|
|
|
1730
1905
|
});
|
|
1731
1906
|
this.destroyRef.onDestroy(() => {
|
|
1732
1907
|
this.clearEditorSessionSubscriptions();
|
|
1733
|
-
this.
|
|
1908
|
+
this.cancelRemoteDataRequest();
|
|
1909
|
+
this.destroyResizeObserver();
|
|
1734
1910
|
this.shellObserver()?.disconnect();
|
|
1911
|
+
this.themeObserver()?.disconnect();
|
|
1735
1912
|
this.cancelScheduledResize();
|
|
1736
1913
|
this.cancelScheduledRender();
|
|
1737
1914
|
this.engine.destroy();
|
|
@@ -1790,13 +1967,45 @@ class PraxisChartComponent {
|
|
|
1790
1967
|
this.pointClick.emit(event);
|
|
1791
1968
|
const config = this.renderConfig();
|
|
1792
1969
|
const interactions = config.interactions;
|
|
1793
|
-
if (!interactions?.selection && !interactions?.crossFilter) {
|
|
1970
|
+
if (!interactions?.pointClick && !interactions?.selection && !interactions?.drillDown && !interactions?.crossFilter) {
|
|
1794
1971
|
return;
|
|
1795
1972
|
}
|
|
1796
1973
|
const selectionEvent = this.buildSelectionEvent(config, event);
|
|
1974
|
+
if (interactions.pointClick) {
|
|
1975
|
+
const pointAction = interactions.eventActions?.pointClick;
|
|
1976
|
+
if (pointAction) {
|
|
1977
|
+
this.pointAction.emit({
|
|
1978
|
+
chartId: config.id ?? event.chartId,
|
|
1979
|
+
event: 'pointClick',
|
|
1980
|
+
point: {
|
|
1981
|
+
...event,
|
|
1982
|
+
chartId: config.id ?? event.chartId,
|
|
1983
|
+
},
|
|
1984
|
+
filters: this.buildEventFilters(config, event, pointAction.mapping),
|
|
1985
|
+
target: pointAction.target,
|
|
1986
|
+
action: pointAction,
|
|
1987
|
+
});
|
|
1988
|
+
}
|
|
1989
|
+
}
|
|
1797
1990
|
if (interactions.selection) {
|
|
1798
1991
|
this.selectionChange.emit(selectionEvent);
|
|
1799
1992
|
}
|
|
1993
|
+
if (interactions.drillDown) {
|
|
1994
|
+
const drillDownAction = interactions.eventActions?.drillDown;
|
|
1995
|
+
if (drillDownAction) {
|
|
1996
|
+
this.drillDown.emit({
|
|
1997
|
+
chartId: config.id ?? event.chartId,
|
|
1998
|
+
filters: this.buildEventFilters(config, event, drillDownAction.mapping),
|
|
1999
|
+
target: drillDownAction.target,
|
|
2000
|
+
action: drillDownAction,
|
|
2001
|
+
source: selectionEvent,
|
|
2002
|
+
point: {
|
|
2003
|
+
...event,
|
|
2004
|
+
chartId: config.id ?? event.chartId,
|
|
2005
|
+
},
|
|
2006
|
+
});
|
|
2007
|
+
}
|
|
2008
|
+
}
|
|
1800
2009
|
if (interactions.crossFilter) {
|
|
1801
2010
|
const crossFilterAction = interactions.eventActions?.crossFilter;
|
|
1802
2011
|
this.crossFilter.emit({
|
|
@@ -1815,6 +2024,7 @@ class PraxisChartComponent {
|
|
|
1815
2024
|
...event,
|
|
1816
2025
|
chartId: config.id ?? event.chartId,
|
|
1817
2026
|
selected: true,
|
|
2027
|
+
mode: 'single',
|
|
1818
2028
|
filters,
|
|
1819
2029
|
action,
|
|
1820
2030
|
};
|
|
@@ -1870,16 +2080,23 @@ class PraxisChartComponent {
|
|
|
1870
2080
|
}
|
|
1871
2081
|
}
|
|
1872
2082
|
ensureResizeObserver(host) {
|
|
1873
|
-
if (this.resizeObserver()) {
|
|
2083
|
+
if (this.resizeObserver() && this.resizeObserverHost === host) {
|
|
1874
2084
|
return;
|
|
1875
2085
|
}
|
|
2086
|
+
this.destroyResizeObserver();
|
|
1876
2087
|
const observer = new ResizeObserver(() => {
|
|
1877
2088
|
this.scheduleChartRender();
|
|
1878
2089
|
this.scheduleEngineResize();
|
|
1879
2090
|
});
|
|
1880
2091
|
observer.observe(host);
|
|
2092
|
+
this.resizeObserverHost = host;
|
|
1881
2093
|
this.resizeObserver.set(observer);
|
|
1882
2094
|
}
|
|
2095
|
+
destroyResizeObserver() {
|
|
2096
|
+
this.resizeObserver()?.disconnect();
|
|
2097
|
+
this.resizeObserver.set(null);
|
|
2098
|
+
this.resizeObserverHost = null;
|
|
2099
|
+
}
|
|
1883
2100
|
hasRenderableSize(host) {
|
|
1884
2101
|
const rect = host.getBoundingClientRect();
|
|
1885
2102
|
const width = rect.width || host.clientWidth || host.offsetWidth;
|
|
@@ -1903,7 +2120,7 @@ class PraxisChartComponent {
|
|
|
1903
2120
|
const trimmed = value?.trim();
|
|
1904
2121
|
return trimmed || null;
|
|
1905
2122
|
}
|
|
1906
|
-
buildRemoteSignature(config) {
|
|
2123
|
+
buildRemoteSignature(config, queryContext) {
|
|
1907
2124
|
if (config.dataSource?.kind !== 'remote') {
|
|
1908
2125
|
return null;
|
|
1909
2126
|
}
|
|
@@ -1911,8 +2128,14 @@ class PraxisChartComponent {
|
|
|
1911
2128
|
id: config.id,
|
|
1912
2129
|
resourcePath: config.dataSource.resourcePath,
|
|
1913
2130
|
query: config.dataSource.query,
|
|
2131
|
+
queryContext,
|
|
1914
2132
|
});
|
|
1915
2133
|
}
|
|
2134
|
+
cancelRemoteDataRequest() {
|
|
2135
|
+
this.remoteRequestSequence += 1;
|
|
2136
|
+
this.remoteRequestSubscription?.unsubscribe();
|
|
2137
|
+
this.remoteRequestSubscription = null;
|
|
2138
|
+
}
|
|
1916
2139
|
clearEditorSessionSubscriptions() {
|
|
1917
2140
|
for (const subscription of this.editorSessionSubscriptions) {
|
|
1918
2141
|
subscription.unsubscribe();
|
|
@@ -1940,6 +2163,100 @@ class PraxisChartComponent {
|
|
|
1940
2163
|
});
|
|
1941
2164
|
this.shellObserver.set(observer);
|
|
1942
2165
|
}
|
|
2166
|
+
observeThemeContext() {
|
|
2167
|
+
const host = this.hostElement.nativeElement;
|
|
2168
|
+
this.syncInheritedTheme(host);
|
|
2169
|
+
if (typeof MutationObserver === 'undefined') {
|
|
2170
|
+
return;
|
|
2171
|
+
}
|
|
2172
|
+
this.themeObserver()?.disconnect();
|
|
2173
|
+
const observer = new MutationObserver(() => {
|
|
2174
|
+
this.syncInheritedTheme(host);
|
|
2175
|
+
});
|
|
2176
|
+
let current = host;
|
|
2177
|
+
while (current) {
|
|
2178
|
+
observer.observe(current, {
|
|
2179
|
+
attributes: true,
|
|
2180
|
+
attributeFilter: [
|
|
2181
|
+
'class',
|
|
2182
|
+
'style',
|
|
2183
|
+
'data-theme',
|
|
2184
|
+
'data-theme-preset',
|
|
2185
|
+
'data-color-scheme',
|
|
2186
|
+
],
|
|
2187
|
+
});
|
|
2188
|
+
current = current.parentElement;
|
|
2189
|
+
}
|
|
2190
|
+
this.themeObserver.set(observer);
|
|
2191
|
+
}
|
|
2192
|
+
syncInheritedTheme(host) {
|
|
2193
|
+
if (typeof getComputedStyle !== 'function') {
|
|
2194
|
+
return;
|
|
2195
|
+
}
|
|
2196
|
+
const pageThemePresetId = host
|
|
2197
|
+
.closest('.pdx-page-wrapper')
|
|
2198
|
+
?.getAttribute('data-theme-preset')
|
|
2199
|
+
?.trim();
|
|
2200
|
+
const chartThemePresetId = pageThemePresetId
|
|
2201
|
+
? BUILTIN_PAGE_THEME_PRESETS[pageThemePresetId]?.chartThemePreset?.trim()
|
|
2202
|
+
: undefined;
|
|
2203
|
+
const preset = chartThemePresetId && this.isThemeVariant(chartThemePresetId)
|
|
2204
|
+
? this.cloneTheme(PRAXIS_CHART_THEME_VARIANTS[chartThemePresetId])
|
|
2205
|
+
: {};
|
|
2206
|
+
const hostTextColor = getComputedStyle(host).color.trim();
|
|
2207
|
+
const theme = {
|
|
2208
|
+
...preset,
|
|
2209
|
+
textColor: preset.textColor || hostTextColor || undefined,
|
|
2210
|
+
};
|
|
2211
|
+
const normalizedTheme = Object.values(theme).some((value) => value !== undefined)
|
|
2212
|
+
? theme
|
|
2213
|
+
: null;
|
|
2214
|
+
const signature = normalizedTheme ? JSON.stringify(normalizedTheme) : null;
|
|
2215
|
+
if (signature === this.inheritedThemeSignature) {
|
|
2216
|
+
return;
|
|
2217
|
+
}
|
|
2218
|
+
this.inheritedThemeSignature = signature;
|
|
2219
|
+
this.inheritedTheme.set(normalizedTheme);
|
|
2220
|
+
}
|
|
2221
|
+
isThemeVariant(value) {
|
|
2222
|
+
return Object.prototype.hasOwnProperty.call(PRAXIS_CHART_THEME_VARIANTS, value);
|
|
2223
|
+
}
|
|
2224
|
+
applyInheritedTheme(config, inherited) {
|
|
2225
|
+
if (!inherited) {
|
|
2226
|
+
return config;
|
|
2227
|
+
}
|
|
2228
|
+
const explicit = config.theme;
|
|
2229
|
+
return {
|
|
2230
|
+
...config,
|
|
2231
|
+
theme: {
|
|
2232
|
+
palette: explicit?.palette ?? inherited.palette,
|
|
2233
|
+
backgroundColor: explicit?.backgroundColor ?? inherited.backgroundColor,
|
|
2234
|
+
textColor: explicit?.textColor ?? inherited.textColor,
|
|
2235
|
+
borderRadius: explicit?.borderRadius ?? inherited.borderRadius,
|
|
2236
|
+
legend: this.mergeOptionalThemeObject(inherited.legend, explicit?.legend),
|
|
2237
|
+
tooltip: this.mergeOptionalThemeObject(inherited.tooltip, explicit?.tooltip),
|
|
2238
|
+
surface: this.mergeOptionalThemeObject(inherited.surface, explicit?.surface),
|
|
2239
|
+
},
|
|
2240
|
+
};
|
|
2241
|
+
}
|
|
2242
|
+
cloneTheme(theme) {
|
|
2243
|
+
return {
|
|
2244
|
+
...theme,
|
|
2245
|
+
palette: theme.palette ? [...theme.palette] : undefined,
|
|
2246
|
+
legend: this.mergeOptionalThemeObject(undefined, theme.legend),
|
|
2247
|
+
tooltip: this.mergeOptionalThemeObject(undefined, theme.tooltip),
|
|
2248
|
+
surface: this.mergeOptionalThemeObject(undefined, theme.surface),
|
|
2249
|
+
};
|
|
2250
|
+
}
|
|
2251
|
+
mergeOptionalThemeObject(defaults, explicit) {
|
|
2252
|
+
if (!defaults && !explicit) {
|
|
2253
|
+
return undefined;
|
|
2254
|
+
}
|
|
2255
|
+
return {
|
|
2256
|
+
...(defaults ?? {}),
|
|
2257
|
+
...(explicit ?? {}),
|
|
2258
|
+
};
|
|
2259
|
+
}
|
|
1943
2260
|
shouldFillShellContainer(shell) {
|
|
1944
2261
|
const rect = shell.getBoundingClientRect();
|
|
1945
2262
|
const hasControlledHeight = (rect.height || shell.clientHeight || shell.offsetHeight) > 0;
|
|
@@ -1953,6 +2270,9 @@ class PraxisChartComponent {
|
|
|
1953
2270
|
this.scheduleEngineResize();
|
|
1954
2271
|
}
|
|
1955
2272
|
scheduleEngineResize() {
|
|
2273
|
+
if (this.loadState() !== 'ready') {
|
|
2274
|
+
return;
|
|
2275
|
+
}
|
|
1956
2276
|
if (typeof requestAnimationFrame !== 'function') {
|
|
1957
2277
|
this.engine.resize();
|
|
1958
2278
|
return;
|
|
@@ -1962,6 +2282,9 @@ class PraxisChartComponent {
|
|
|
1962
2282
|
}
|
|
1963
2283
|
this.resizeFrameId = requestAnimationFrame(() => {
|
|
1964
2284
|
this.resizeFrameId = null;
|
|
2285
|
+
if (this.loadState() !== 'ready') {
|
|
2286
|
+
return;
|
|
2287
|
+
}
|
|
1965
2288
|
this.engine.resize();
|
|
1966
2289
|
});
|
|
1967
2290
|
}
|
|
@@ -2002,7 +2325,7 @@ class PraxisChartComponent {
|
|
|
2002
2325
|
this.resizeFrameId = null;
|
|
2003
2326
|
}
|
|
2004
2327
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: PraxisChartComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
2005
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.14", type: PraxisChartComponent, isStandalone: true, selector: "praxis-chart", inputs: { config: { classPropertyName: "config", publicName: "config", isSignal: true, isRequired: false, transformFunction: null }, data: { classPropertyName: "data", publicName: "data", isSignal: true, isRequired: false, transformFunction: null }, chartDocument: { classPropertyName: "chartDocument", publicName: "chartDocument", isSignal: true, isRequired: false, transformFunction: null }, filterCriteria: { classPropertyName: "filterCriteria", publicName: "filterCriteria", isSignal: true, isRequired: false, transformFunction: null }, queryContext: { classPropertyName: "queryContext", publicName: "queryContext", isSignal: true, isRequired: false, transformFunction: null }, remoteDataResolver: { classPropertyName: "remoteDataResolver", publicName: "remoteDataResolver", isSignal: true, isRequired: false, transformFunction: null }, enableCustomization: { classPropertyName: "enableCustomization", publicName: "enableCustomization", isSignal: true, isRequired: false, transformFunction: null }, availableResources: { classPropertyName: "availableResources", publicName: "availableResources", isSignal: true, isRequired: false, transformFunction: null }, availableFields: { classPropertyName: "availableFields", publicName: "availableFields", isSignal: true, isRequired: false, transformFunction: null }, availableTargets: { classPropertyName: "availableTargets", publicName: "availableTargets", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { pointClick: "pointClick", selectionChange: "selectionChange", crossFilter: "crossFilter", queryRequest: "queryRequest", loadStateChange: "loadStateChange", chartDocumentApplied: "chartDocumentApplied", chartDocumentSaved: "chartDocumentSaved" }, host: { properties: { "class.praxis-chart-host-fill-container": "isFillContainerMode()" } }, viewQueries: [{ propertyName: "chartHost", first: true, predicate: ["chartHost"], descendants: true, isSignal: true }], ngImport: i0, template: `
|
|
2328
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.14", type: PraxisChartComponent, isStandalone: true, selector: "praxis-chart", inputs: { config: { classPropertyName: "config", publicName: "config", isSignal: true, isRequired: false, transformFunction: null }, data: { classPropertyName: "data", publicName: "data", isSignal: true, isRequired: false, transformFunction: null }, chartDocument: { classPropertyName: "chartDocument", publicName: "chartDocument", isSignal: true, isRequired: false, transformFunction: null }, filterCriteria: { classPropertyName: "filterCriteria", publicName: "filterCriteria", isSignal: true, isRequired: false, transformFunction: null }, queryContext: { classPropertyName: "queryContext", publicName: "queryContext", isSignal: true, isRequired: false, transformFunction: null }, remoteDataResolver: { classPropertyName: "remoteDataResolver", publicName: "remoteDataResolver", isSignal: true, isRequired: false, transformFunction: null }, enableCustomization: { classPropertyName: "enableCustomization", publicName: "enableCustomization", isSignal: true, isRequired: false, transformFunction: null }, availableResources: { classPropertyName: "availableResources", publicName: "availableResources", isSignal: true, isRequired: false, transformFunction: null }, availableFields: { classPropertyName: "availableFields", publicName: "availableFields", isSignal: true, isRequired: false, transformFunction: null }, availableTargets: { classPropertyName: "availableTargets", publicName: "availableTargets", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { pointClick: "pointClick", pointAction: "pointAction", selectionChange: "selectionChange", drillDown: "drillDown", crossFilter: "crossFilter", queryRequest: "queryRequest", loadStateChange: "loadStateChange", chartDocumentApplied: "chartDocumentApplied", chartDocumentSaved: "chartDocumentSaved" }, host: { properties: { "class.praxis-chart-host-fill-container": "isFillContainerMode()" } }, viewQueries: [{ propertyName: "chartHost", first: true, predicate: ["chartHost"], descendants: true, isSignal: true }], ngImport: i0, template: `
|
|
2006
2329
|
<section
|
|
2007
2330
|
class="praxis-chart-shell"
|
|
2008
2331
|
[class.praxis-chart-shell-fill-container]="isFillContainerMode()"
|
|
@@ -2076,7 +2399,7 @@ class PraxisChartComponent {
|
|
|
2076
2399
|
<div #chartHost class="praxis-chart-host"></div>
|
|
2077
2400
|
}
|
|
2078
2401
|
</section>
|
|
2079
|
-
`, isInline: true, styles: [":host{display:block;min-height:var(--praxis-chart-runtime-height, 320px);min-width:0}:host(.praxis-chart-host-fill-container){height:100%;min-height:0}:host-context(.pdx-shell.no-shell) .praxis-chart-shell,:host-context(.pdx-shell.body-fill) .praxis-chart-shell,:host-context(.pdx-shell.expanded) .praxis-chart-shell,:host-context(.pdx-shell.fullscreen) .praxis-chart-shell{height:100%!important}.praxis-chart-shell{position:relative;width:100%;height:100%;min-height:240px;border-radius:var(--praxis-chart-config-surface-radius, var(--praxis-chart-surface-radius, 8px));overflow:hidden;background:var( --praxis-chart-config-surface-bg, var(--praxis-chart-surface-bg, var(--md-sys-color-surface-container-lowest, #fff)) );border-color:var( --praxis-chart-config-surface-border, var( --praxis-chart-surface-border, color-mix(in srgb, var(--md-sys-color-outline, #c5c7ce) 44%, transparent) ) );border-style:solid;border-width:var(--praxis-chart-config-surface-border-width, 1px)}.praxis-chart-shell-fill-container{min-height:0}:host-context(.pdx-shell) .praxis-chart-shell:not(.praxis-chart-shell-contained),.praxis-chart-shell-embedded{border-width:var( --praxis-chart-config-surface-border-width, var(--praxis-chart-embedded-surface-border-width, 0) );border-radius:var( --praxis-chart-config-surface-radius, var(--praxis-chart-embedded-surface-radius, 0) );background:var( --praxis-chart-config-surface-bg, var(--praxis-chart-embedded-surface-bg, transparent) )}.praxis-chart-settings-trigger{position:absolute;top:10px;right:10px;z-index:3;background:color-mix(in srgb,var(--md-sys-color-surface, #fff) 88%,rgba(18,99,180,.12));-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px)}.praxis-chart-host{width:100%;height:100%;min-height:inherit}.praxis-chart-state{height:100%;min-height:min(240px,100%);display:grid;align-content:center;justify-items:center;gap:14px;padding:24px;text-align:center}.praxis-chart-state-copy{display:grid;gap:6px;justify-items:center}.praxis-chart-state-title{font-size:1rem;font-weight:600;color:var(--md-sys-color-on-surface, #1a1b20)}.praxis-chart-state-description{font-size:.925rem;color:var(--md-sys-color-on-surface-variant, #5a5d67);max-width:36rem}.praxis-chart-loading-hero{width:min(100%,320px);display:grid;gap:14px}.praxis-chart-loading-summary{display:flex;gap:8px;justify-content:center}.praxis-chart-loading-chip,.praxis-chart-loading-bar{border-radius:999px;background:linear-gradient(90deg,#1263b414,#1263b438,#1263b414);background-size:200% 100%;animation:praxis-chart-loading-wave 1.2s ease-in-out infinite}.praxis-chart-loading-chip{display:block;width:104px;height:12px}.praxis-chart-loading-chip--short{width:64px}.praxis-chart-loading-plot{display:grid;grid-template-columns:repeat(5,minmax(0,1fr));align-items:end;gap:10px;height:108px;padding:12px 8px 4px;border-radius:8px;background:color-mix(in srgb,var(--md-sys-color-surface-container, #eef3f8) 72%,transparent);border:1px solid rgba(18,99,180,.08)}.praxis-chart-loading-bar{display:block;width:100%}.praxis-chart-loading-bar--1{height:32%}.praxis-chart-loading-bar--2{height:68%}.praxis-chart-loading-bar--3{height:48%}.praxis-chart-loading-bar--4{height:78%}.praxis-chart-loading-bar--5{height:56%}@keyframes praxis-chart-loading-wave{0%{background-position:100% 0}to{background-position:-100% 0}}\n"], dependencies: [{ kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i1$2.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i2.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatTooltipModule }, { kind: "directive", type: i3.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
2402
|
+
`, isInline: true, styles: [":host{display:block;min-height:var(--praxis-chart-runtime-height, 320px);min-width:0;color:var(--md-sys-color-on-surface, #1a1b20)}:host(.praxis-chart-host-fill-container){height:100%;min-height:0}:host-context(.pdx-shell.no-shell) .praxis-chart-shell,:host-context(.pdx-shell.body-fill) .praxis-chart-shell,:host-context(.pdx-shell.expanded) .praxis-chart-shell,:host-context(.pdx-shell.fullscreen) .praxis-chart-shell{height:100%!important}.praxis-chart-shell{position:relative;width:100%;height:100%;min-height:240px;border-radius:var(--praxis-chart-config-surface-radius, var(--praxis-chart-surface-radius, 8px));overflow:hidden;background:var( --praxis-chart-config-surface-bg, var(--praxis-chart-surface-bg, var(--md-sys-color-surface-container-lowest, #fff)) );border-color:var( --praxis-chart-config-surface-border, var( --praxis-chart-surface-border, color-mix(in srgb, var(--md-sys-color-outline, #c5c7ce) 44%, transparent) ) );border-style:solid;border-width:var(--praxis-chart-config-surface-border-width, 1px)}.praxis-chart-shell-fill-container{min-height:0}:host-context(.pdx-shell) .praxis-chart-shell:not(.praxis-chart-shell-contained),.praxis-chart-shell-embedded{border-width:var( --praxis-chart-config-surface-border-width, var(--praxis-chart-embedded-surface-border-width, 0) );border-radius:var( --praxis-chart-config-surface-radius, var(--praxis-chart-embedded-surface-radius, 0) );background:var( --praxis-chart-config-surface-bg, var(--praxis-chart-embedded-surface-bg, transparent) )}.praxis-chart-settings-trigger{position:absolute;top:10px;right:10px;z-index:3;background:color-mix(in srgb,var(--md-sys-color-surface, #fff) 88%,rgba(18,99,180,.12));-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px)}.praxis-chart-host{width:100%;height:100%;min-height:inherit}.praxis-chart-state{height:100%;min-height:min(240px,100%);display:grid;align-content:center;justify-items:center;gap:14px;padding:24px;text-align:center}.praxis-chart-state-copy{display:grid;gap:6px;justify-items:center}.praxis-chart-state-title{font-size:1rem;font-weight:600;color:var(--md-sys-color-on-surface, #1a1b20)}.praxis-chart-state-description{font-size:.925rem;color:var(--md-sys-color-on-surface-variant, #5a5d67);max-width:36rem}.praxis-chart-loading-hero{width:min(100%,320px);display:grid;gap:14px}.praxis-chart-loading-summary{display:flex;gap:8px;justify-content:center}.praxis-chart-loading-chip,.praxis-chart-loading-bar{border-radius:999px;background:linear-gradient(90deg,#1263b414,#1263b438,#1263b414);background-size:200% 100%;animation:praxis-chart-loading-wave 1.2s ease-in-out infinite}.praxis-chart-loading-chip{display:block;width:104px;height:12px}.praxis-chart-loading-chip--short{width:64px}.praxis-chart-loading-plot{display:grid;grid-template-columns:repeat(5,minmax(0,1fr));align-items:end;gap:10px;height:108px;padding:12px 8px 4px;border-radius:8px;background:color-mix(in srgb,var(--md-sys-color-surface-container, #eef3f8) 72%,transparent);border:1px solid rgba(18,99,180,.08)}.praxis-chart-loading-bar{display:block;width:100%}.praxis-chart-loading-bar--1{height:32%}.praxis-chart-loading-bar--2{height:68%}.praxis-chart-loading-bar--3{height:48%}.praxis-chart-loading-bar--4{height:78%}.praxis-chart-loading-bar--5{height:56%}@keyframes praxis-chart-loading-wave{0%{background-position:100% 0}to{background-position:-100% 0}}\n"], dependencies: [{ kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i1$2.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i2.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatTooltipModule }, { kind: "directive", type: i3.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
2080
2403
|
}
|
|
2081
2404
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: PraxisChartComponent, decorators: [{
|
|
2082
2405
|
type: Component,
|
|
@@ -2156,65 +2479,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
|
|
|
2156
2479
|
<div #chartHost class="praxis-chart-host"></div>
|
|
2157
2480
|
}
|
|
2158
2481
|
</section>
|
|
2159
|
-
`, styles: [":host{display:block;min-height:var(--praxis-chart-runtime-height, 320px);min-width:0}:host(.praxis-chart-host-fill-container){height:100%;min-height:0}:host-context(.pdx-shell.no-shell) .praxis-chart-shell,:host-context(.pdx-shell.body-fill) .praxis-chart-shell,:host-context(.pdx-shell.expanded) .praxis-chart-shell,:host-context(.pdx-shell.fullscreen) .praxis-chart-shell{height:100%!important}.praxis-chart-shell{position:relative;width:100%;height:100%;min-height:240px;border-radius:var(--praxis-chart-config-surface-radius, var(--praxis-chart-surface-radius, 8px));overflow:hidden;background:var( --praxis-chart-config-surface-bg, var(--praxis-chart-surface-bg, var(--md-sys-color-surface-container-lowest, #fff)) );border-color:var( --praxis-chart-config-surface-border, var( --praxis-chart-surface-border, color-mix(in srgb, var(--md-sys-color-outline, #c5c7ce) 44%, transparent) ) );border-style:solid;border-width:var(--praxis-chart-config-surface-border-width, 1px)}.praxis-chart-shell-fill-container{min-height:0}:host-context(.pdx-shell) .praxis-chart-shell:not(.praxis-chart-shell-contained),.praxis-chart-shell-embedded{border-width:var( --praxis-chart-config-surface-border-width, var(--praxis-chart-embedded-surface-border-width, 0) );border-radius:var( --praxis-chart-config-surface-radius, var(--praxis-chart-embedded-surface-radius, 0) );background:var( --praxis-chart-config-surface-bg, var(--praxis-chart-embedded-surface-bg, transparent) )}.praxis-chart-settings-trigger{position:absolute;top:10px;right:10px;z-index:3;background:color-mix(in srgb,var(--md-sys-color-surface, #fff) 88%,rgba(18,99,180,.12));-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px)}.praxis-chart-host{width:100%;height:100%;min-height:inherit}.praxis-chart-state{height:100%;min-height:min(240px,100%);display:grid;align-content:center;justify-items:center;gap:14px;padding:24px;text-align:center}.praxis-chart-state-copy{display:grid;gap:6px;justify-items:center}.praxis-chart-state-title{font-size:1rem;font-weight:600;color:var(--md-sys-color-on-surface, #1a1b20)}.praxis-chart-state-description{font-size:.925rem;color:var(--md-sys-color-on-surface-variant, #5a5d67);max-width:36rem}.praxis-chart-loading-hero{width:min(100%,320px);display:grid;gap:14px}.praxis-chart-loading-summary{display:flex;gap:8px;justify-content:center}.praxis-chart-loading-chip,.praxis-chart-loading-bar{border-radius:999px;background:linear-gradient(90deg,#1263b414,#1263b438,#1263b414);background-size:200% 100%;animation:praxis-chart-loading-wave 1.2s ease-in-out infinite}.praxis-chart-loading-chip{display:block;width:104px;height:12px}.praxis-chart-loading-chip--short{width:64px}.praxis-chart-loading-plot{display:grid;grid-template-columns:repeat(5,minmax(0,1fr));align-items:end;gap:10px;height:108px;padding:12px 8px 4px;border-radius:8px;background:color-mix(in srgb,var(--md-sys-color-surface-container, #eef3f8) 72%,transparent);border:1px solid rgba(18,99,180,.08)}.praxis-chart-loading-bar{display:block;width:100%}.praxis-chart-loading-bar--1{height:32%}.praxis-chart-loading-bar--2{height:68%}.praxis-chart-loading-bar--3{height:48%}.praxis-chart-loading-bar--4{height:78%}.praxis-chart-loading-bar--5{height:56%}@keyframes praxis-chart-loading-wave{0%{background-position:100% 0}to{background-position:-100% 0}}\n"] }]
|
|
2160
|
-
}], ctorParameters: () => [], propDecorators: { config: [{ type: i0.Input, args: [{ isSignal: true, alias: "config", required: false }] }], data: [{ type: i0.Input, args: [{ isSignal: true, alias: "data", required: false }] }], chartDocument: [{ type: i0.Input, args: [{ isSignal: true, alias: "chartDocument", required: false }] }], filterCriteria: [{ type: i0.Input, args: [{ isSignal: true, alias: "filterCriteria", required: false }] }], queryContext: [{ type: i0.Input, args: [{ isSignal: true, alias: "queryContext", required: false }] }], remoteDataResolver: [{ type: i0.Input, args: [{ isSignal: true, alias: "remoteDataResolver", required: false }] }], enableCustomization: [{ type: i0.Input, args: [{ isSignal: true, alias: "enableCustomization", required: false }] }], availableResources: [{ type: i0.Input, args: [{ isSignal: true, alias: "availableResources", required: false }] }], availableFields: [{ type: i0.Input, args: [{ isSignal: true, alias: "availableFields", required: false }] }], availableTargets: [{ type: i0.Input, args: [{ isSignal: true, alias: "availableTargets", required: false }] }], pointClick: [{ type: i0.Output, args: ["pointClick"] }], selectionChange: [{ type: i0.Output, args: ["selectionChange"] }], crossFilter: [{ type: i0.Output, args: ["crossFilter"] }], queryRequest: [{ type: i0.Output, args: ["queryRequest"] }], loadStateChange: [{ type: i0.Output, args: ["loadStateChange"] }], chartDocumentApplied: [{ type: i0.Output, args: ["chartDocumentApplied"] }], chartDocumentSaved: [{ type: i0.Output, args: ["chartDocumentSaved"] }], chartHost: [{ type: i0.ViewChild, args: ['chartHost', { isSignal: true }] }] } });
|
|
2161
|
-
function normalizeFilterCriteria(criteria) {
|
|
2162
|
-
if (!criteria)
|
|
2163
|
-
return null;
|
|
2164
|
-
const next = Object.entries(criteria).reduce((acc, [key, value]) => {
|
|
2165
|
-
if (value === null || value === undefined)
|
|
2166
|
-
return acc;
|
|
2167
|
-
if (Array.isArray(value) && value.length === 0)
|
|
2168
|
-
return acc;
|
|
2169
|
-
if (typeof value === 'string' && value.trim() === '')
|
|
2170
|
-
return acc;
|
|
2171
|
-
acc[key] = value;
|
|
2172
|
-
return acc;
|
|
2173
|
-
}, {});
|
|
2174
|
-
return Object.keys(next).length ? next : null;
|
|
2175
|
-
}
|
|
2176
|
-
function normalizePraxisDataQueryContextBridge(context) {
|
|
2177
|
-
if (!context || typeof context !== 'object')
|
|
2178
|
-
return null;
|
|
2179
|
-
const filters = normalizeFilterCriteria(context.filters || null);
|
|
2180
|
-
const meta = normalizeFilterCriteria(context.meta || null);
|
|
2181
|
-
const sort = Array.isArray(context.sort)
|
|
2182
|
-
? context.sort
|
|
2183
|
-
.map((item) => (typeof item === 'string' ? item.trim() : ''))
|
|
2184
|
-
.filter((item) => item.length > 0)
|
|
2185
|
-
: [];
|
|
2186
|
-
const limit = typeof context.limit === 'number' && Number.isFinite(context.limit)
|
|
2187
|
-
? Math.max(0, Math.trunc(context.limit))
|
|
2188
|
-
: null;
|
|
2189
|
-
const page = context.page && typeof context.page === 'object'
|
|
2190
|
-
? {
|
|
2191
|
-
index: typeof context.page.index === 'number' && Number.isFinite(context.page.index)
|
|
2192
|
-
? Math.max(0, Math.trunc(context.page.index))
|
|
2193
|
-
: null,
|
|
2194
|
-
size: typeof context.page.size === 'number' && Number.isFinite(context.page.size)
|
|
2195
|
-
? Math.max(0, Math.trunc(context.page.size))
|
|
2196
|
-
: null,
|
|
2197
|
-
}
|
|
2198
|
-
: null;
|
|
2199
|
-
const next = {};
|
|
2200
|
-
if (filters)
|
|
2201
|
-
next.filters = filters;
|
|
2202
|
-
if (sort.length)
|
|
2203
|
-
next.sort = sort;
|
|
2204
|
-
if (limit !== null)
|
|
2205
|
-
next.limit = limit;
|
|
2206
|
-
if (page && (page.index !== null || page.size !== null))
|
|
2207
|
-
next.page = page;
|
|
2208
|
-
if (meta)
|
|
2209
|
-
next.meta = meta;
|
|
2210
|
-
return Object.keys(next).length ? next : null;
|
|
2211
|
-
}
|
|
2212
|
-
function resolvePraxisFilterCriteriaBridge(filterCriteria, queryContext) {
|
|
2213
|
-
return {
|
|
2214
|
-
...(normalizeFilterCriteria(filterCriteria) || {}),
|
|
2215
|
-
...(normalizePraxisDataQueryContextBridge(queryContext)?.filters || {}),
|
|
2216
|
-
};
|
|
2217
|
-
}
|
|
2482
|
+
`, styles: [":host{display:block;min-height:var(--praxis-chart-runtime-height, 320px);min-width:0;color:var(--md-sys-color-on-surface, #1a1b20)}:host(.praxis-chart-host-fill-container){height:100%;min-height:0}:host-context(.pdx-shell.no-shell) .praxis-chart-shell,:host-context(.pdx-shell.body-fill) .praxis-chart-shell,:host-context(.pdx-shell.expanded) .praxis-chart-shell,:host-context(.pdx-shell.fullscreen) .praxis-chart-shell{height:100%!important}.praxis-chart-shell{position:relative;width:100%;height:100%;min-height:240px;border-radius:var(--praxis-chart-config-surface-radius, var(--praxis-chart-surface-radius, 8px));overflow:hidden;background:var( --praxis-chart-config-surface-bg, var(--praxis-chart-surface-bg, var(--md-sys-color-surface-container-lowest, #fff)) );border-color:var( --praxis-chart-config-surface-border, var( --praxis-chart-surface-border, color-mix(in srgb, var(--md-sys-color-outline, #c5c7ce) 44%, transparent) ) );border-style:solid;border-width:var(--praxis-chart-config-surface-border-width, 1px)}.praxis-chart-shell-fill-container{min-height:0}:host-context(.pdx-shell) .praxis-chart-shell:not(.praxis-chart-shell-contained),.praxis-chart-shell-embedded{border-width:var( --praxis-chart-config-surface-border-width, var(--praxis-chart-embedded-surface-border-width, 0) );border-radius:var( --praxis-chart-config-surface-radius, var(--praxis-chart-embedded-surface-radius, 0) );background:var( --praxis-chart-config-surface-bg, var(--praxis-chart-embedded-surface-bg, transparent) )}.praxis-chart-settings-trigger{position:absolute;top:10px;right:10px;z-index:3;background:color-mix(in srgb,var(--md-sys-color-surface, #fff) 88%,rgba(18,99,180,.12));-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px)}.praxis-chart-host{width:100%;height:100%;min-height:inherit}.praxis-chart-state{height:100%;min-height:min(240px,100%);display:grid;align-content:center;justify-items:center;gap:14px;padding:24px;text-align:center}.praxis-chart-state-copy{display:grid;gap:6px;justify-items:center}.praxis-chart-state-title{font-size:1rem;font-weight:600;color:var(--md-sys-color-on-surface, #1a1b20)}.praxis-chart-state-description{font-size:.925rem;color:var(--md-sys-color-on-surface-variant, #5a5d67);max-width:36rem}.praxis-chart-loading-hero{width:min(100%,320px);display:grid;gap:14px}.praxis-chart-loading-summary{display:flex;gap:8px;justify-content:center}.praxis-chart-loading-chip,.praxis-chart-loading-bar{border-radius:999px;background:linear-gradient(90deg,#1263b414,#1263b438,#1263b414);background-size:200% 100%;animation:praxis-chart-loading-wave 1.2s ease-in-out infinite}.praxis-chart-loading-chip{display:block;width:104px;height:12px}.praxis-chart-loading-chip--short{width:64px}.praxis-chart-loading-plot{display:grid;grid-template-columns:repeat(5,minmax(0,1fr));align-items:end;gap:10px;height:108px;padding:12px 8px 4px;border-radius:8px;background:color-mix(in srgb,var(--md-sys-color-surface-container, #eef3f8) 72%,transparent);border:1px solid rgba(18,99,180,.08)}.praxis-chart-loading-bar{display:block;width:100%}.praxis-chart-loading-bar--1{height:32%}.praxis-chart-loading-bar--2{height:68%}.praxis-chart-loading-bar--3{height:48%}.praxis-chart-loading-bar--4{height:78%}.praxis-chart-loading-bar--5{height:56%}@keyframes praxis-chart-loading-wave{0%{background-position:100% 0}to{background-position:-100% 0}}\n"] }]
|
|
2483
|
+
}], ctorParameters: () => [], propDecorators: { config: [{ type: i0.Input, args: [{ isSignal: true, alias: "config", required: false }] }], data: [{ type: i0.Input, args: [{ isSignal: true, alias: "data", required: false }] }], chartDocument: [{ type: i0.Input, args: [{ isSignal: true, alias: "chartDocument", required: false }] }], filterCriteria: [{ type: i0.Input, args: [{ isSignal: true, alias: "filterCriteria", required: false }] }], queryContext: [{ type: i0.Input, args: [{ isSignal: true, alias: "queryContext", required: false }] }], remoteDataResolver: [{ type: i0.Input, args: [{ isSignal: true, alias: "remoteDataResolver", required: false }] }], enableCustomization: [{ type: i0.Input, args: [{ isSignal: true, alias: "enableCustomization", required: false }] }], availableResources: [{ type: i0.Input, args: [{ isSignal: true, alias: "availableResources", required: false }] }], availableFields: [{ type: i0.Input, args: [{ isSignal: true, alias: "availableFields", required: false }] }], availableTargets: [{ type: i0.Input, args: [{ isSignal: true, alias: "availableTargets", required: false }] }], pointClick: [{ type: i0.Output, args: ["pointClick"] }], pointAction: [{ type: i0.Output, args: ["pointAction"] }], selectionChange: [{ type: i0.Output, args: ["selectionChange"] }], drillDown: [{ type: i0.Output, args: ["drillDown"] }], crossFilter: [{ type: i0.Output, args: ["crossFilter"] }], queryRequest: [{ type: i0.Output, args: ["queryRequest"] }], loadStateChange: [{ type: i0.Output, args: ["loadStateChange"] }], chartDocumentApplied: [{ type: i0.Output, args: ["chartDocumentApplied"] }], chartDocumentSaved: [{ type: i0.Output, args: ["chartDocumentSaved"] }], chartHost: [{ type: i0.ViewChild, args: ['chartHost', { isSignal: true }] }] } });
|
|
2218
2484
|
function mergeRemoteQueryContext(config, queryContext, runtimeFilters) {
|
|
2219
2485
|
const dataSource = config.dataSource;
|
|
2220
2486
|
if (dataSource?.kind !== 'remote' || !dataSource.query)
|
|
@@ -2247,6 +2513,17 @@ function mergeRemoteQueryContext(config, queryContext, runtimeFilters) {
|
|
|
2247
2513
|
},
|
|
2248
2514
|
};
|
|
2249
2515
|
}
|
|
2516
|
+
function resolveUnsupportedStatsFilterExpressionDiagnostic(queryContext) {
|
|
2517
|
+
if (!queryContext?.filterExpression) {
|
|
2518
|
+
return null;
|
|
2519
|
+
}
|
|
2520
|
+
const governance = queryContext.filterExpression.governance;
|
|
2521
|
+
const decision = governance?.decisionId ? ` Decision: ${governance.decisionId}.` : '';
|
|
2522
|
+
const reason = queryContext.filterExpression.projection?.reason
|
|
2523
|
+
? ` Projection reason: ${queryContext.filterExpression.projection.reason}.`
|
|
2524
|
+
: '';
|
|
2525
|
+
return `Praxis Charts received a governed queryContext.filterExpression, but praxis.stats execution currently accepts only flat statsRequest.filter criteria. The expression was rejected instead of being flattened into an unsafe AND filter.${decision}${reason}`;
|
|
2526
|
+
}
|
|
2250
2527
|
function normalizeRemoteDataResolverResult$1(result) {
|
|
2251
2528
|
if (Array.isArray(result)) {
|
|
2252
2529
|
return of(result);
|
|
@@ -3248,7 +3525,7 @@ class PraxisChartDrilldownPanelComponent {
|
|
|
3248
3525
|
[data]="detailData()"
|
|
3249
3526
|
></praxis-chart>
|
|
3250
3527
|
</section>
|
|
3251
|
-
`, isInline: true, styles: [":host{display:block}.drilldown-shell{display:grid;gap:14px}.drilldown-header h3,.drilldown-eyebrow,.drilldown-description{margin:0}.drilldown-eyebrow{font-size:.75rem;letter-spacing:.14em;text-transform:uppercase;color:var(--md-sys-color-on-surface-variant, #5a5d67)}.drilldown-header h3{font-size:1.2rem;color:var(--md-sys-color-on-surface, #1a1b20)}.drilldown-description{color:var(--md-sys-color-on-surface-variant, #5a5d67)}\n"], dependencies: [{ kind: "component", type: PraxisChartComponent, selector: "praxis-chart", inputs: ["config", "data", "chartDocument", "filterCriteria", "queryContext", "remoteDataResolver", "enableCustomization", "availableResources", "availableFields", "availableTargets"], outputs: ["pointClick", "selectionChange", "crossFilter", "queryRequest", "loadStateChange", "chartDocumentApplied", "chartDocumentSaved"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
3528
|
+
`, isInline: true, styles: [":host{display:block}.drilldown-shell{display:grid;gap:14px}.drilldown-header h3,.drilldown-eyebrow,.drilldown-description{margin:0}.drilldown-eyebrow{font-size:.75rem;letter-spacing:.14em;text-transform:uppercase;color:var(--md-sys-color-on-surface-variant, #5a5d67)}.drilldown-header h3{font-size:1.2rem;color:var(--md-sys-color-on-surface, #1a1b20)}.drilldown-description{color:var(--md-sys-color-on-surface-variant, #5a5d67)}\n"], dependencies: [{ kind: "component", type: PraxisChartComponent, selector: "praxis-chart", inputs: ["config", "data", "chartDocument", "filterCriteria", "queryContext", "remoteDataResolver", "enableCustomization", "availableResources", "availableFields", "availableTargets"], outputs: ["pointClick", "pointAction", "selectionChange", "drillDown", "crossFilter", "queryRequest", "loadStateChange", "chartDocumentApplied", "chartDocumentSaved"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
3252
3529
|
}
|
|
3253
3530
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: PraxisChartDrilldownPanelComponent, decorators: [{
|
|
3254
3531
|
type: Component,
|
|
@@ -3318,7 +3595,7 @@ const CARTESIAN_GRID_TOP_WITHOUT_TITLE = 48;
|
|
|
3318
3595
|
const CARTESIAN_GRID_BOTTOM_WITH_LEGEND = 64;
|
|
3319
3596
|
const CARTESIAN_GRID_BOTTOM_WITHOUT_LEGEND = 40;
|
|
3320
3597
|
const DEFAULT_VALUE_LOCALE = 'pt-BR';
|
|
3321
|
-
class
|
|
3598
|
+
class EChartsOptionBuilderService {
|
|
3322
3599
|
transformer;
|
|
3323
3600
|
constructor(transformer) {
|
|
3324
3601
|
this.transformer = transformer;
|
|
@@ -3431,19 +3708,14 @@ class PraxisChartOptionBuilderService {
|
|
|
3431
3708
|
};
|
|
3432
3709
|
}
|
|
3433
3710
|
const horizontal = config.orientation === 'horizontal' || config.type === 'horizontal-bar';
|
|
3434
|
-
const xAxisType = config
|
|
3711
|
+
const xAxisType = this.usesCanonicalStatsTime(config)
|
|
3712
|
+
? 'time'
|
|
3713
|
+
: config.axes?.x?.type ?? 'category';
|
|
3435
3714
|
return {
|
|
3436
3715
|
backgroundColor: config.theme?.backgroundColor,
|
|
3437
3716
|
color: palette,
|
|
3438
3717
|
title: this.buildTitle(config, 'left', textColor),
|
|
3439
|
-
tooltip: tooltipEnabled
|
|
3440
|
-
? {
|
|
3441
|
-
trigger: config.theme?.tooltip?.trigger ?? 'axis',
|
|
3442
|
-
confine: true,
|
|
3443
|
-
appendToBody: false,
|
|
3444
|
-
valueFormatter: (value) => this.formatValue(value, this.primaryValueFormat(config)),
|
|
3445
|
-
}
|
|
3446
|
-
: undefined,
|
|
3718
|
+
tooltip: tooltipEnabled ? this.buildCartesianTooltip(config) : undefined,
|
|
3447
3719
|
legend: this.buildLegend(config, legendVisible, 'bottom', textColor),
|
|
3448
3720
|
grid: this.buildGrid(config, { horizontal, legendVisible }),
|
|
3449
3721
|
xAxis: horizontal
|
|
@@ -3663,6 +3935,87 @@ class PraxisChartOptionBuilderService {
|
|
|
3663
3935
|
?? config.axes?.y?.labels?.format
|
|
3664
3936
|
?? config.series[0]?.labels?.format;
|
|
3665
3937
|
}
|
|
3938
|
+
buildCartesianTooltip(config) {
|
|
3939
|
+
const tooltip = {
|
|
3940
|
+
trigger: config.theme?.tooltip?.trigger ?? 'axis',
|
|
3941
|
+
confine: true,
|
|
3942
|
+
appendToBody: false,
|
|
3943
|
+
valueFormatter: (value) => this.formatValue(value, this.primaryValueFormat(config)),
|
|
3944
|
+
};
|
|
3945
|
+
return this.usesCanonicalStatsTime(config)
|
|
3946
|
+
? {
|
|
3947
|
+
...tooltip,
|
|
3948
|
+
formatter: (params) => this.formatCanonicalTimeTooltip(params, config),
|
|
3949
|
+
}
|
|
3950
|
+
: tooltip;
|
|
3951
|
+
}
|
|
3952
|
+
usesCanonicalStatsTime(config) {
|
|
3953
|
+
if (config.dataSource?.kind !== 'remote'
|
|
3954
|
+
|| config.dataSource.query?.sourceKind !== 'praxis.stats') {
|
|
3955
|
+
return false;
|
|
3956
|
+
}
|
|
3957
|
+
return config.axes?.x?.type === 'time'
|
|
3958
|
+
|| config.dataSource.query.statsOperation === 'timeseries';
|
|
3959
|
+
}
|
|
3960
|
+
formatCanonicalTimeTooltip(params, config) {
|
|
3961
|
+
const items = (Array.isArray(params) ? params : [params])
|
|
3962
|
+
.filter((item) => !!item && typeof item === 'object');
|
|
3963
|
+
if (!items.length) {
|
|
3964
|
+
return '';
|
|
3965
|
+
}
|
|
3966
|
+
const first = items[0];
|
|
3967
|
+
const data = this.tooltipPointData(first['data']);
|
|
3968
|
+
const header = this.formatCanonicalTimeTooltipHeader(first, data, config);
|
|
3969
|
+
const lines = items.map((item) => {
|
|
3970
|
+
const itemData = this.tooltipPointData(item['data']);
|
|
3971
|
+
const seriesId = item['seriesId'] === null || item['seriesId'] === undefined
|
|
3972
|
+
? undefined
|
|
3973
|
+
: String(item['seriesId']);
|
|
3974
|
+
const format = seriesId
|
|
3975
|
+
? this.seriesLabelFormat(config, seriesId) ?? this.primaryValueFormat(config)
|
|
3976
|
+
: this.primaryValueFormat(config);
|
|
3977
|
+
const value = this.formatValue(item['value'] ?? itemData['value'], format);
|
|
3978
|
+
const seriesName = item['seriesName'] === null || item['seriesName'] === undefined
|
|
3979
|
+
? ''
|
|
3980
|
+
: String(item['seriesName']);
|
|
3981
|
+
return seriesName
|
|
3982
|
+
? `${escapeTooltipHtml(seriesName)}: ${escapeTooltipHtml(value)}`
|
|
3983
|
+
: escapeTooltipHtml(value);
|
|
3984
|
+
});
|
|
3985
|
+
return [escapeTooltipHtml(header), ...lines]
|
|
3986
|
+
.filter((part) => part.length > 0)
|
|
3987
|
+
.join('<br/>');
|
|
3988
|
+
}
|
|
3989
|
+
formatCanonicalTimeTooltipHeader(params, data, config) {
|
|
3990
|
+
const dateFormat = config.axes?.x?.labels?.format;
|
|
3991
|
+
const label = this.nonEmptyText(data['label']);
|
|
3992
|
+
const start = this.nonEmptyText(data['start']);
|
|
3993
|
+
const end = this.nonEmptyText(data['end']);
|
|
3994
|
+
const formattedStart = start ? this.formatValue(start, dateFormat) : '';
|
|
3995
|
+
const formattedEnd = end ? this.formatValue(end, dateFormat) : '';
|
|
3996
|
+
const interval = formattedStart && formattedEnd && formattedStart !== formattedEnd
|
|
3997
|
+
? `${formattedStart} – ${formattedEnd}`
|
|
3998
|
+
: formattedStart || formattedEnd;
|
|
3999
|
+
const fallback = this.nonEmptyText(params['axisValueLabel'])
|
|
4000
|
+
?? this.nonEmptyText(params['name'])
|
|
4001
|
+
?? '';
|
|
4002
|
+
if (label && interval && label !== formattedStart && label !== interval) {
|
|
4003
|
+
return `${label} · ${interval}`;
|
|
4004
|
+
}
|
|
4005
|
+
return interval || label || fallback;
|
|
4006
|
+
}
|
|
4007
|
+
tooltipPointData(value) {
|
|
4008
|
+
return value && typeof value === 'object' && !Array.isArray(value)
|
|
4009
|
+
? value
|
|
4010
|
+
: {};
|
|
4011
|
+
}
|
|
4012
|
+
nonEmptyText(value) {
|
|
4013
|
+
if (value === null || value === undefined) {
|
|
4014
|
+
return undefined;
|
|
4015
|
+
}
|
|
4016
|
+
const text = String(value).trim();
|
|
4017
|
+
return text || undefined;
|
|
4018
|
+
}
|
|
3666
4019
|
seriesLabelFormat(config, seriesId) {
|
|
3667
4020
|
return config.series.find((series) => series.id === seriesId)?.labels?.format;
|
|
3668
4021
|
}
|
|
@@ -3811,10 +4164,10 @@ class PraxisChartOptionBuilderService {
|
|
|
3811
4164
|
}
|
|
3812
4165
|
return Math.min(Math.max(value, 0), 20);
|
|
3813
4166
|
}
|
|
3814
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type:
|
|
3815
|
-
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type:
|
|
4167
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: EChartsOptionBuilderService, deps: [{ token: PraxisChartDataTransformerService }], target: i0.ɵɵFactoryTarget.Injectable });
|
|
4168
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: EChartsOptionBuilderService, providedIn: 'root' });
|
|
3816
4169
|
}
|
|
3817
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type:
|
|
4170
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: EChartsOptionBuilderService, decorators: [{
|
|
3818
4171
|
type: Injectable,
|
|
3819
4172
|
args: [{ providedIn: 'root' }]
|
|
3820
4173
|
}], ctorParameters: () => [{ type: PraxisChartDataTransformerService }] });
|
|
@@ -3825,6 +4178,14 @@ function truncateLegendText(name) {
|
|
|
3825
4178
|
}
|
|
3826
4179
|
return `${value.slice(0, 25)}...`;
|
|
3827
4180
|
}
|
|
4181
|
+
function escapeTooltipHtml(value) {
|
|
4182
|
+
return String(value ?? '')
|
|
4183
|
+
.replace(/&/g, '&')
|
|
4184
|
+
.replace(/</g, '<')
|
|
4185
|
+
.replace(/>/g, '>')
|
|
4186
|
+
.replace(/"/g, '"')
|
|
4187
|
+
.replace(/'/g, ''');
|
|
4188
|
+
}
|
|
3828
4189
|
|
|
3829
4190
|
use([
|
|
3830
4191
|
AriaComponent,
|
|
@@ -3843,38 +4204,42 @@ use([
|
|
|
3843
4204
|
class EChartsEngineAdapter {
|
|
3844
4205
|
optionBuilder;
|
|
3845
4206
|
chart;
|
|
4207
|
+
currentHost;
|
|
3846
4208
|
clickHost;
|
|
3847
4209
|
domClickHandler;
|
|
3848
4210
|
zrenderClickHandler;
|
|
3849
4211
|
lastSeriesClickAt = 0;
|
|
4212
|
+
lastGridClick;
|
|
3850
4213
|
constructor(optionBuilder) {
|
|
3851
4214
|
this.optionBuilder = optionBuilder;
|
|
3852
4215
|
}
|
|
3853
4216
|
render(host, payload) {
|
|
4217
|
+
if (this.chart && this.currentHost !== host) {
|
|
4218
|
+
this.disposeChart();
|
|
4219
|
+
}
|
|
3854
4220
|
if (!this.chart) {
|
|
3855
|
-
this.chart =
|
|
4221
|
+
this.chart = this.createChart(host);
|
|
4222
|
+
this.currentHost = host;
|
|
3856
4223
|
}
|
|
3857
4224
|
const option = this.optionBuilder.build(payload.config, payload.data);
|
|
3858
|
-
this.chart
|
|
3859
|
-
|
|
3860
|
-
this.
|
|
4225
|
+
const chart = this.chart;
|
|
4226
|
+
chart.setOption(option, true);
|
|
4227
|
+
this.detachHandlers();
|
|
4228
|
+
chart.off('click');
|
|
4229
|
+
chart.on('click', (params) => {
|
|
3861
4230
|
this.lastSeriesClickAt = Date.now();
|
|
4231
|
+
this.lastGridClick = undefined;
|
|
4232
|
+
const data = pointData(params?.name, params?.data ?? params?.value);
|
|
3862
4233
|
payload.onPointClick?.({
|
|
3863
4234
|
chartId: payload.config.id,
|
|
3864
4235
|
seriesId: params?.seriesId,
|
|
3865
4236
|
seriesName: params?.seriesName,
|
|
3866
|
-
category: params?.name,
|
|
4237
|
+
category: this.resolvePointCategory(payload, data, params?.name),
|
|
3867
4238
|
value: pointValue(params?.value ?? params?.data),
|
|
3868
|
-
data
|
|
4239
|
+
data,
|
|
3869
4240
|
});
|
|
3870
4241
|
});
|
|
3871
|
-
const zrender =
|
|
3872
|
-
if (zrender && this.zrenderClickHandler) {
|
|
3873
|
-
zrender.off?.('click', this.zrenderClickHandler);
|
|
3874
|
-
}
|
|
3875
|
-
if (this.clickHost && this.domClickHandler) {
|
|
3876
|
-
this.clickHost.removeEventListener('click', this.domClickHandler, true);
|
|
3877
|
-
}
|
|
4242
|
+
const zrender = chart?.getZr?.();
|
|
3878
4243
|
this.zrenderClickHandler = (event) => {
|
|
3879
4244
|
window.setTimeout(() => {
|
|
3880
4245
|
if (Date.now() - this.lastSeriesClickAt < 80) {
|
|
@@ -3905,6 +4270,21 @@ class EChartsEngineAdapter {
|
|
|
3905
4270
|
this.chart?.resize();
|
|
3906
4271
|
}
|
|
3907
4272
|
destroy() {
|
|
4273
|
+
this.disposeChart();
|
|
4274
|
+
}
|
|
4275
|
+
createChart(host) {
|
|
4276
|
+
return init(host);
|
|
4277
|
+
}
|
|
4278
|
+
disposeChart() {
|
|
4279
|
+
this.detachHandlers();
|
|
4280
|
+
this.chart?.off('click');
|
|
4281
|
+
this.chart?.dispose();
|
|
4282
|
+
this.chart = undefined;
|
|
4283
|
+
this.currentHost = undefined;
|
|
4284
|
+
this.lastSeriesClickAt = 0;
|
|
4285
|
+
this.lastGridClick = undefined;
|
|
4286
|
+
}
|
|
4287
|
+
detachHandlers() {
|
|
3908
4288
|
const zrender = this.chart?.getZr?.();
|
|
3909
4289
|
if (zrender && this.zrenderClickHandler) {
|
|
3910
4290
|
zrender.off?.('click', this.zrenderClickHandler);
|
|
@@ -3915,8 +4295,26 @@ class EChartsEngineAdapter {
|
|
|
3915
4295
|
this.clickHost = undefined;
|
|
3916
4296
|
this.domClickHandler = undefined;
|
|
3917
4297
|
this.zrenderClickHandler = undefined;
|
|
3918
|
-
|
|
3919
|
-
|
|
4298
|
+
}
|
|
4299
|
+
resolvePointCategory(payload, data, fallback) {
|
|
4300
|
+
const categoryField = payload.config.axes?.x?.field
|
|
4301
|
+
?? payload.config.series.find((series) => series.categoryField)?.categoryField;
|
|
4302
|
+
const dataSource = payload.config.dataSource;
|
|
4303
|
+
const usesCanonicalStatsTime = dataSource?.kind === 'remote'
|
|
4304
|
+
&& dataSource.query?.sourceKind === 'praxis.stats';
|
|
4305
|
+
const usesCanonicalStatsTimePoint = usesCanonicalStatsTime
|
|
4306
|
+
&& (payload.config.axes?.x?.type === 'time'
|
|
4307
|
+
|| (dataSource?.kind === 'remote' && dataSource.query?.statsOperation === 'timeseries'));
|
|
4308
|
+
const candidate = usesCanonicalStatsTimePoint && data['label'] !== null
|
|
4309
|
+
&& data['label'] !== undefined
|
|
4310
|
+
&& data['label'] !== ''
|
|
4311
|
+
? data['label']
|
|
4312
|
+
: categoryField && data[categoryField] !== null && data[categoryField] !== undefined
|
|
4313
|
+
? data[categoryField]
|
|
4314
|
+
: fallback;
|
|
4315
|
+
return candidate === null || candidate === undefined
|
|
4316
|
+
? undefined
|
|
4317
|
+
: String(candidate);
|
|
3920
4318
|
}
|
|
3921
4319
|
emitCategoryClickFromGrid(event, payload) {
|
|
3922
4320
|
const chart = this.chart;
|
|
@@ -3944,23 +4342,54 @@ class EChartsEngineAdapter {
|
|
|
3944
4342
|
return;
|
|
3945
4343
|
}
|
|
3946
4344
|
const category = categories[categoryIndex];
|
|
3947
|
-
const
|
|
3948
|
-
const
|
|
4345
|
+
const categoryValue = category == null ? undefined : String(category);
|
|
4346
|
+
const categoryField = payload.config.axes?.x?.field
|
|
4347
|
+
?? payload.config.series.find((series) => series.categoryField)?.categoryField;
|
|
4348
|
+
const signature = JSON.stringify({
|
|
4349
|
+
chartId: payload.config.id,
|
|
4350
|
+
category: categoryValue,
|
|
4351
|
+
horizontal,
|
|
4352
|
+
categoryIndex,
|
|
4353
|
+
});
|
|
4354
|
+
if (this.isDuplicateGridClick(signature)) {
|
|
4355
|
+
return;
|
|
4356
|
+
}
|
|
4357
|
+
const usesCanonicalStatsRows = payload.config.dataSource?.kind === 'remote'
|
|
4358
|
+
&& payload.config.dataSource.query?.sourceKind === 'praxis.stats';
|
|
4359
|
+
const fallbackSourceRow = !usesCanonicalStatsRows
|
|
4360
|
+
&& categoryField
|
|
4361
|
+
&& categoryValue !== undefined
|
|
4362
|
+
? payload.data.find((row) => String(row[categoryField] ?? '') === categoryValue)
|
|
4363
|
+
: undefined;
|
|
4364
|
+
const sourceRow = usesCanonicalStatsRows
|
|
4365
|
+
? payload.data[categoryIndex]
|
|
4366
|
+
: fallbackSourceRow;
|
|
4367
|
+
const data = {
|
|
4368
|
+
...(sourceRow ?? {}),
|
|
4369
|
+
category: categoryValue,
|
|
4370
|
+
};
|
|
4371
|
+
if (categoryField && categoryValue !== undefined) {
|
|
4372
|
+
data[categoryField] = categoryValue;
|
|
4373
|
+
}
|
|
3949
4374
|
payload.onPointClick?.({
|
|
3950
4375
|
chartId: payload.config.id,
|
|
3951
|
-
|
|
3952
|
-
|
|
3953
|
-
category: category == null ? undefined : String(category),
|
|
3954
|
-
value: pointValue(value),
|
|
3955
|
-
data: pointData(category, value),
|
|
4376
|
+
category: categoryValue,
|
|
4377
|
+
data,
|
|
3956
4378
|
});
|
|
3957
4379
|
}
|
|
3958
|
-
|
|
4380
|
+
isDuplicateGridClick(signature) {
|
|
4381
|
+
const now = Date.now();
|
|
4382
|
+
const duplicate = this.lastGridClick?.signature === signature
|
|
4383
|
+
&& now - this.lastGridClick.at < 80;
|
|
4384
|
+
this.lastGridClick = { signature, at: now };
|
|
4385
|
+
return duplicate;
|
|
4386
|
+
}
|
|
4387
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: EChartsEngineAdapter, deps: [{ token: EChartsOptionBuilderService }], target: i0.ɵɵFactoryTarget.Injectable });
|
|
3959
4388
|
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: EChartsEngineAdapter });
|
|
3960
4389
|
}
|
|
3961
4390
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: EChartsEngineAdapter, decorators: [{
|
|
3962
4391
|
type: Injectable
|
|
3963
|
-
}], ctorParameters: () => [{ type:
|
|
4392
|
+
}], ctorParameters: () => [{ type: EChartsOptionBuilderService }] });
|
|
3964
4393
|
function firstOptionEntry(value) {
|
|
3965
4394
|
return Array.isArray(value) ? value[0] : value;
|
|
3966
4395
|
}
|
|
@@ -3990,6 +4419,13 @@ const PRAXIS_CHARTS_EN_US = {
|
|
|
3990
4419
|
'praxis.charts.runtime.remoteErrorTitle': 'Chart data could not be loaded',
|
|
3991
4420
|
'praxis.charts.runtime.remoteErrorDescription': 'Review the chart data source and analytics request before continuing.',
|
|
3992
4421
|
'praxis.charts.runtime.booleanFalseLabel': 'Not {{ label }}',
|
|
4422
|
+
'praxis.charts.runtime.comparisonCurrent': 'Current',
|
|
4423
|
+
'praxis.charts.runtime.comparisonPrevious': 'Previous',
|
|
4424
|
+
'praxis.charts.widget.missingDocument': 'This widget does not have a canonical chart document yet. Runtime inputs are preserved until a canonical chartDocument is provided by the host.',
|
|
4425
|
+
'praxis.charts.widget.queryContext.label': 'Query context',
|
|
4426
|
+
'praxis.charts.widget.queryContext.ariaLabel': 'Chart query context',
|
|
4427
|
+
'praxis.charts.widget.queryContext.invalidJson': 'Query context must be valid JSON.',
|
|
4428
|
+
'praxis.charts.widget.queryContext.mustBeObject': 'Query context must be a JSON object.',
|
|
3993
4429
|
'praxis.charts.editor.section.general': 'General',
|
|
3994
4430
|
'praxis.charts.editor.section.data': 'Data',
|
|
3995
4431
|
'praxis.charts.editor.section.analytics': 'Analytics',
|
|
@@ -4051,6 +4487,34 @@ const PRAXIS_CHARTS_EN_US = {
|
|
|
4051
4487
|
'praxis.charts.editor.appearance.surfaceTitle': 'Surface',
|
|
4052
4488
|
'praxis.charts.editor.appearance.surfaceHint': 'Use embedded for dashboard widgets and contained only when the chart must own its visual surface.',
|
|
4053
4489
|
'praxis.charts.editor.appearance.statesTitle': 'State messages',
|
|
4490
|
+
'praxis.charts.editor.catalog.resourceMissing': 'Resource catalog is required for governed praxis.stats authoring.',
|
|
4491
|
+
'praxis.charts.editor.catalog.resourceStale': 'Selected resource is not available in the governed resource catalog.',
|
|
4492
|
+
'praxis.charts.editor.catalog.operationMissing': 'Selected resource does not publish authorable stats operations.',
|
|
4493
|
+
'praxis.charts.editor.catalog.operationStale': 'Selected operation is not supported by the governed resource catalog.',
|
|
4494
|
+
'praxis.charts.editor.catalog.fieldCatalogMissing': 'The selected resource does not publish governed fields for chart authoring.',
|
|
4495
|
+
'praxis.charts.editor.catalog.dimensionFieldsMissing': 'No governed dimensions are available for the current resource and operation.',
|
|
4496
|
+
'praxis.charts.editor.catalog.metricFieldsMissing': 'No governed metrics are available for the current resource and operation.',
|
|
4497
|
+
'praxis.charts.editor.catalog.dimensionFieldStale': 'Selected dimension is not eligible for the current resource and operation.',
|
|
4498
|
+
'praxis.charts.editor.catalog.metricFieldStale': 'Selected metric is not eligible for the current resource and operation.',
|
|
4499
|
+
'praxis.charts.editor.catalog.metricAggregationStale': 'Selected aggregation is not allowed for the selected metric field.',
|
|
4500
|
+
'praxis.charts.editor.catalog.targetMissing': 'Target catalog is required for this governed event action.',
|
|
4501
|
+
'praxis.charts.editor.catalog.targetStale': 'Selected target is not available for this event action.',
|
|
4502
|
+
'praxis.charts.editor.catalog.capabilitiesDenied': 'Access to the selected resource capabilities was denied.',
|
|
4503
|
+
'praxis.charts.editor.catalog.capabilitiesUnavailable': 'The selected resource capabilities are temporarily unavailable.',
|
|
4504
|
+
'praxis.charts.editor.catalog.contextUnavailable': 'Governed chart authoring context could not be resolved.',
|
|
4505
|
+
'praxis.charts.widget.contextDiagnostics.title': 'Authoring context',
|
|
4506
|
+
'praxis.charts.widget.contextDiagnostics.catalogDenied': 'Access to the governed resource catalog was denied.',
|
|
4507
|
+
'praxis.charts.widget.contextDiagnostics.catalogUnavailable': 'The governed resource catalog is temporarily unavailable.',
|
|
4508
|
+
'praxis.charts.widget.contextDiagnostics.catalogEmpty': 'No authorable stats resources were published by the governed catalog.',
|
|
4509
|
+
'praxis.charts.widget.contextDiagnostics.catalogPathInvalid': 'The resource catalog published an unsafe or non-relative operational path.',
|
|
4510
|
+
'praxis.charts.widget.contextDiagnostics.capabilitiesDenied': 'Access to the selected resource capabilities was denied.',
|
|
4511
|
+
'praxis.charts.widget.contextDiagnostics.capabilitiesUnavailable': 'The selected resource capabilities are temporarily unavailable.',
|
|
4512
|
+
'praxis.charts.widget.contextDiagnostics.operationsEmpty': 'The selected resource does not publish authorable stats operations.',
|
|
4513
|
+
'praxis.charts.widget.contextDiagnostics.fieldsEmpty': 'The selected resource does not publish governed fields for chart authoring.',
|
|
4514
|
+
'praxis.charts.widget.contextDiagnostics.identityMismatch': 'The selected resource identity does not match its governed capability snapshot.',
|
|
4515
|
+
'praxis.charts.widget.contextDiagnostics.capabilitiesPathInvalid': 'The selected capability snapshot did not publish a safe governed resource path.',
|
|
4516
|
+
'praxis.charts.widget.contextDiagnostics.capabilitiesPathMismatch': 'The selected capability snapshot path does not match the operational path published by the catalog.',
|
|
4517
|
+
'praxis.charts.widget.contextDiagnostics.partial': 'Some governed authoring choices could not be materialized from the current page context.',
|
|
4054
4518
|
'praxis.charts.editor.hint.sizingMode': 'Use fill-container only when the host widget provides a defined body height.',
|
|
4055
4519
|
'praxis.charts.editor.hint.height': 'Numbers are saved as pixels; CSS lengths such as 20rem are also accepted.',
|
|
4056
4520
|
'praxis.charts.editor.hint.minHeight': 'Set a readable minimum for compact dashboard widgets.',
|
|
@@ -4143,6 +4607,13 @@ const PRAXIS_CHARTS_PT_BR = {
|
|
|
4143
4607
|
'praxis.charts.runtime.remoteErrorTitle': 'Não foi possível carregar os dados do gráfico',
|
|
4144
4608
|
'praxis.charts.runtime.remoteErrorDescription': 'Revise a fonte de dados e os filtros antes de continuar.',
|
|
4145
4609
|
'praxis.charts.runtime.booleanFalseLabel': 'Não {{ label }}',
|
|
4610
|
+
'praxis.charts.runtime.comparisonCurrent': 'Atual',
|
|
4611
|
+
'praxis.charts.runtime.comparisonPrevious': 'Anterior',
|
|
4612
|
+
'praxis.charts.widget.missingDocument': 'Este widget ainda não possui um documento canônico de gráfico. Os inputs runtime são preservados até o host fornecer um chartDocument canônico.',
|
|
4613
|
+
'praxis.charts.widget.queryContext.label': 'Contexto da consulta',
|
|
4614
|
+
'praxis.charts.widget.queryContext.ariaLabel': 'Contexto da consulta do gráfico',
|
|
4615
|
+
'praxis.charts.widget.queryContext.invalidJson': 'O contexto da consulta deve ser um JSON válido.',
|
|
4616
|
+
'praxis.charts.widget.queryContext.mustBeObject': 'O contexto da consulta deve ser um objeto JSON.',
|
|
4146
4617
|
'praxis.charts.editor.section.general': 'Geral',
|
|
4147
4618
|
'praxis.charts.editor.section.data': 'Dados',
|
|
4148
4619
|
'praxis.charts.editor.section.analytics': 'Estrutura',
|
|
@@ -4204,6 +4675,34 @@ const PRAXIS_CHARTS_PT_BR = {
|
|
|
4204
4675
|
'praxis.charts.editor.appearance.surfaceTitle': 'Superfície',
|
|
4205
4676
|
'praxis.charts.editor.appearance.surfaceHint': 'Use embutido para widgets de dashboard e contido apenas quando o gráfico precisar ser dono da própria superfície visual.',
|
|
4206
4677
|
'praxis.charts.editor.appearance.statesTitle': 'Mensagens de estado',
|
|
4678
|
+
'praxis.charts.editor.catalog.resourceMissing': 'O catálogo de recursos é obrigatório para authoring governado de praxis.stats.',
|
|
4679
|
+
'praxis.charts.editor.catalog.resourceStale': 'O recurso selecionado não está disponível no catálogo governado de recursos.',
|
|
4680
|
+
'praxis.charts.editor.catalog.operationMissing': 'O recurso selecionado não publica operações de stats authoráveis.',
|
|
4681
|
+
'praxis.charts.editor.catalog.operationStale': 'A operação selecionada não é suportada pelo catálogo governado do recurso.',
|
|
4682
|
+
'praxis.charts.editor.catalog.fieldCatalogMissing': 'O recurso selecionado não publica campos governados para a autoria do gráfico.',
|
|
4683
|
+
'praxis.charts.editor.catalog.dimensionFieldsMissing': 'Nenhuma dimensão governada está disponível para o recurso e a operação atuais.',
|
|
4684
|
+
'praxis.charts.editor.catalog.metricFieldsMissing': 'Nenhuma métrica governada está disponível para o recurso e a operação atuais.',
|
|
4685
|
+
'praxis.charts.editor.catalog.dimensionFieldStale': 'A dimensão selecionada não é elegível para o recurso e a operação atuais.',
|
|
4686
|
+
'praxis.charts.editor.catalog.metricFieldStale': 'A métrica selecionada não é elegível para o recurso e a operação atuais.',
|
|
4687
|
+
'praxis.charts.editor.catalog.metricAggregationStale': 'A agregação selecionada não é permitida para a métrica escolhida.',
|
|
4688
|
+
'praxis.charts.editor.catalog.targetMissing': 'O catálogo de destinos é obrigatório para esta ação de evento governada.',
|
|
4689
|
+
'praxis.charts.editor.catalog.targetStale': 'O destino selecionado não está disponível para esta ação de evento.',
|
|
4690
|
+
'praxis.charts.editor.catalog.capabilitiesDenied': 'O acesso às capabilities do recurso selecionado foi negado.',
|
|
4691
|
+
'praxis.charts.editor.catalog.capabilitiesUnavailable': 'As capabilities do recurso selecionado estão temporariamente indisponíveis.',
|
|
4692
|
+
'praxis.charts.editor.catalog.contextUnavailable': 'Não foi possível resolver o contexto governado de authoring do gráfico.',
|
|
4693
|
+
'praxis.charts.widget.contextDiagnostics.title': 'Contexto de authoring',
|
|
4694
|
+
'praxis.charts.widget.contextDiagnostics.catalogDenied': 'O acesso ao catálogo governado de recursos foi negado.',
|
|
4695
|
+
'praxis.charts.widget.contextDiagnostics.catalogUnavailable': 'O catálogo governado de recursos está temporariamente indisponível.',
|
|
4696
|
+
'praxis.charts.widget.contextDiagnostics.catalogEmpty': 'Nenhum recurso de stats authorável foi publicado pelo catálogo governado.',
|
|
4697
|
+
'praxis.charts.widget.contextDiagnostics.catalogPathInvalid': 'O catálogo de recursos publicou um path operacional inseguro ou não relativo.',
|
|
4698
|
+
'praxis.charts.widget.contextDiagnostics.capabilitiesDenied': 'O acesso às capabilities do recurso selecionado foi negado.',
|
|
4699
|
+
'praxis.charts.widget.contextDiagnostics.capabilitiesUnavailable': 'As capabilities do recurso selecionado estão temporariamente indisponíveis.',
|
|
4700
|
+
'praxis.charts.widget.contextDiagnostics.operationsEmpty': 'O recurso selecionado não publica operações de stats authoráveis.',
|
|
4701
|
+
'praxis.charts.widget.contextDiagnostics.fieldsEmpty': 'O recurso selecionado não publica campos governados para a autoria do gráfico.',
|
|
4702
|
+
'praxis.charts.widget.contextDiagnostics.identityMismatch': 'A identidade do recurso selecionado não corresponde ao snapshot governado de capabilities.',
|
|
4703
|
+
'praxis.charts.widget.contextDiagnostics.capabilitiesPathInvalid': 'O snapshot de capabilities selecionado não publicou um path de recurso governado e seguro.',
|
|
4704
|
+
'praxis.charts.widget.contextDiagnostics.capabilitiesPathMismatch': 'O path do snapshot de capabilities não corresponde ao path operacional publicado pelo catálogo.',
|
|
4705
|
+
'praxis.charts.widget.contextDiagnostics.partial': 'Algumas opções governadas de authoring não puderam ser materializadas a partir do contexto atual da página.',
|
|
4207
4706
|
'praxis.charts.editor.hint.sizingMode': 'Use preencher container apenas quando o widget host fornecer altura definida para o corpo.',
|
|
4208
4707
|
'praxis.charts.editor.hint.height': 'Números são salvos como pixels; medidas CSS como 20rem também são aceitas.',
|
|
4209
4708
|
'praxis.charts.editor.hint.minHeight': 'Defina um mínimo legível para widgets compactos de dashboard.',
|
|
@@ -4342,7 +4841,7 @@ function resolvePraxisChartsText(value, fallback) {
|
|
|
4342
4841
|
class ChartEditorDefaultsService {
|
|
4343
4842
|
create() {
|
|
4344
4843
|
return {
|
|
4345
|
-
version:
|
|
4844
|
+
version: PRAXIS_X_UI_CHART_AUTHORABLE_VERSION,
|
|
4346
4845
|
kind: 'bar',
|
|
4347
4846
|
title: 'Untitled chart',
|
|
4348
4847
|
source: {
|
|
@@ -4418,13 +4917,844 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
|
|
|
4418
4917
|
args: [{ providedIn: 'root' }]
|
|
4419
4918
|
}], ctorParameters: () => [{ type: PraxisChartCanonicalContractMapperService }] });
|
|
4420
4919
|
|
|
4920
|
+
/**
|
|
4921
|
+
* Internal projection of already-governed Page Builder composition evidence.
|
|
4922
|
+
*
|
|
4923
|
+
* It deliberately does not discover unconnected widgets, nested widgets,
|
|
4924
|
+
* routes, surfaces, or global actions. A target enters this catalog only when
|
|
4925
|
+
* an existing top-level link from the current chart proves the destination.
|
|
4926
|
+
*/
|
|
4927
|
+
class ChartCompositionTargetCatalogAdapter {
|
|
4928
|
+
registry = inject(ComponentMetadataRegistry);
|
|
4929
|
+
resolve(request) {
|
|
4930
|
+
const targets = new Map();
|
|
4931
|
+
const diagnostics = [];
|
|
4932
|
+
const diagnosticKeys = new Set();
|
|
4933
|
+
const addDiagnostic = (code, severity, path) => {
|
|
4934
|
+
const key = `${code}:${path}`;
|
|
4935
|
+
if (diagnosticKeys.has(key)) {
|
|
4936
|
+
return;
|
|
4937
|
+
}
|
|
4938
|
+
diagnosticKeys.add(key);
|
|
4939
|
+
diagnostics.push({ code, severity, path });
|
|
4940
|
+
};
|
|
4941
|
+
if (!this.isPageEvidence(request.page)) {
|
|
4942
|
+
addDiagnostic('chart-target-page-invalid', 'error', 'page');
|
|
4943
|
+
return { targets: [], diagnostics };
|
|
4944
|
+
}
|
|
4945
|
+
const page = request.page;
|
|
4946
|
+
const currentWidget = page.widgets.find((candidate) => this.isWidgetEvidence(candidate) && candidate.key === request.widgetKey);
|
|
4947
|
+
if (!currentWidget) {
|
|
4948
|
+
addDiagnostic('chart-target-source-widget-missing', 'error', 'page.widgets');
|
|
4949
|
+
return { targets: [], diagnostics };
|
|
4950
|
+
}
|
|
4951
|
+
for (const [index, candidate] of (page.composition?.links ?? []).entries()) {
|
|
4952
|
+
if (this.sourceWidgetKey(candidate) !== request.widgetKey) {
|
|
4953
|
+
continue;
|
|
4954
|
+
}
|
|
4955
|
+
const linkPath = `page.composition.links[${index}]`;
|
|
4956
|
+
if (!this.isRecord(candidate)) {
|
|
4957
|
+
addDiagnostic('chart-target-link-invalid', 'warning', linkPath);
|
|
4958
|
+
continue;
|
|
4959
|
+
}
|
|
4960
|
+
if (this.isDeprecatedLink(candidate)) {
|
|
4961
|
+
addDiagnostic('chart-target-link-deprecated', 'warning', linkPath);
|
|
4962
|
+
continue;
|
|
4963
|
+
}
|
|
4964
|
+
const source = this.readComponentPortEndpoint(candidate['from']);
|
|
4965
|
+
if (!source || source.ref.direction !== 'output') {
|
|
4966
|
+
addDiagnostic('chart-target-source-endpoint-invalid', 'warning', `${linkPath}.from`);
|
|
4967
|
+
continue;
|
|
4968
|
+
}
|
|
4969
|
+
if (source.ref.nestedPath?.length) {
|
|
4970
|
+
addDiagnostic('chart-target-source-nested-unsupported', 'info', `${linkPath}.from`);
|
|
4971
|
+
continue;
|
|
4972
|
+
}
|
|
4973
|
+
if (!this.endpointComponentTypeMatches(source.ref.componentType, currentWidget.definition.id, request.widgetType, request.componentId, request.metadata.id, request.metadata.componentType)) {
|
|
4974
|
+
addDiagnostic('chart-target-source-component-type-mismatch', 'warning', `${linkPath}.from`);
|
|
4975
|
+
continue;
|
|
4976
|
+
}
|
|
4977
|
+
const sourcePort = request.metadata.ports?.find((port) => port.id === source.ref.port);
|
|
4978
|
+
if (!this.isAuthorablePort(sourcePort, 'output')) {
|
|
4979
|
+
addDiagnostic('chart-target-source-port-not-authorable', 'warning', `${linkPath}.from`);
|
|
4980
|
+
continue;
|
|
4981
|
+
}
|
|
4982
|
+
const sourceEvent = this.sourceEvent(sourcePort);
|
|
4983
|
+
if (!sourceEvent) {
|
|
4984
|
+
addDiagnostic('chart-target-source-event-unsupported', 'info', `${linkPath}.from.ref.port`);
|
|
4985
|
+
continue;
|
|
4986
|
+
}
|
|
4987
|
+
const target = candidate['to'];
|
|
4988
|
+
if (this.hasEndpointKind(target, 'component-port')) {
|
|
4989
|
+
this.resolveWidgetTarget(target, sourceEvent, sourcePort, candidate['transform'], page, request.widgetKey, linkPath, targets, addDiagnostic);
|
|
4990
|
+
continue;
|
|
4991
|
+
}
|
|
4992
|
+
if (this.hasEndpointKind(target, 'state')) {
|
|
4993
|
+
this.resolveStateTarget(target, sourceEvent, candidate['intent'], page, linkPath, targets, addDiagnostic);
|
|
4994
|
+
continue;
|
|
4995
|
+
}
|
|
4996
|
+
if (this.hasEndpointKind(target, 'global-action')) {
|
|
4997
|
+
addDiagnostic('chart-target-global-action-unsupported', 'info', `${linkPath}.to`);
|
|
4998
|
+
continue;
|
|
4999
|
+
}
|
|
5000
|
+
addDiagnostic('chart-target-endpoint-unsupported', 'warning', `${linkPath}.to`);
|
|
5001
|
+
}
|
|
5002
|
+
return {
|
|
5003
|
+
targets: Array.from(targets.values()),
|
|
5004
|
+
diagnostics,
|
|
5005
|
+
};
|
|
5006
|
+
}
|
|
5007
|
+
resolveWidgetTarget(value, sourceEvent, sourcePort, transform, page, sourceWidgetKey, linkPath, targets, addDiagnostic) {
|
|
5008
|
+
const endpoint = this.readComponentPortEndpoint(value);
|
|
5009
|
+
if (!endpoint || endpoint.ref.direction !== 'input') {
|
|
5010
|
+
addDiagnostic('chart-target-widget-endpoint-invalid', 'warning', `${linkPath}.to`);
|
|
5011
|
+
return;
|
|
5012
|
+
}
|
|
5013
|
+
if (endpoint.ref.nestedPath?.length) {
|
|
5014
|
+
addDiagnostic('chart-target-widget-nested-unsupported', 'info', `${linkPath}.to`);
|
|
5015
|
+
return;
|
|
5016
|
+
}
|
|
5017
|
+
const widget = page.widgets.find((candidate) => this.isWidgetEvidence(candidate) && candidate.key === endpoint.ref.widget);
|
|
5018
|
+
if (!widget) {
|
|
5019
|
+
addDiagnostic('chart-target-widget-missing', 'warning', `${linkPath}.to`);
|
|
5020
|
+
return;
|
|
5021
|
+
}
|
|
5022
|
+
if (widget.key === sourceWidgetKey) {
|
|
5023
|
+
addDiagnostic('chart-target-widget-self-unsupported', 'warning', `${linkPath}.to`);
|
|
5024
|
+
return;
|
|
5025
|
+
}
|
|
5026
|
+
const metadata = this.registry.get(widget.definition.id);
|
|
5027
|
+
if (!metadata) {
|
|
5028
|
+
addDiagnostic('chart-target-widget-metadata-missing', 'warning', `${linkPath}.to`);
|
|
5029
|
+
return;
|
|
5030
|
+
}
|
|
5031
|
+
if (!this.endpointComponentTypeMatches(endpoint.ref.componentType, widget.definition.id, metadata.id, metadata.componentType)) {
|
|
5032
|
+
addDiagnostic('chart-target-widget-component-type-mismatch', 'warning', `${linkPath}.to`);
|
|
5033
|
+
return;
|
|
5034
|
+
}
|
|
5035
|
+
const port = metadata.ports?.find((candidate) => candidate.id === endpoint.ref.port);
|
|
5036
|
+
if (!this.isAuthorablePort(port, 'input')
|
|
5037
|
+
|| port?.semanticKind !== 'query-context') {
|
|
5038
|
+
addDiagnostic('chart-target-widget-port-not-authorable', 'warning', `${linkPath}.to`);
|
|
5039
|
+
return;
|
|
5040
|
+
}
|
|
5041
|
+
if (!this.isWidgetLinkSemanticallyCompatible(sourcePort, port, transform)) {
|
|
5042
|
+
addDiagnostic('chart-target-widget-link-incompatible', 'warning', linkPath);
|
|
5043
|
+
return;
|
|
5044
|
+
}
|
|
5045
|
+
const id = widget.key;
|
|
5046
|
+
this.upsertTarget(targets, `filter-widget:${id}`, {
|
|
5047
|
+
id,
|
|
5048
|
+
label: this.widgetLabel(metadata.friendlyName, id),
|
|
5049
|
+
kind: 'widget',
|
|
5050
|
+
actions: ['filter-widget'],
|
|
5051
|
+
events: [sourceEvent],
|
|
5052
|
+
}, sourceEvent);
|
|
5053
|
+
}
|
|
5054
|
+
resolveStateTarget(value, sourceEvent, intent, page, linkPath, targets, addDiagnostic) {
|
|
5055
|
+
const endpoint = this.readStateEndpoint(value);
|
|
5056
|
+
if (!endpoint || !endpoint.ref.path || endpoint.ref.path.trim() !== endpoint.ref.path) {
|
|
5057
|
+
addDiagnostic('chart-target-state-endpoint-invalid', 'warning', `${linkPath}.to`);
|
|
5058
|
+
return;
|
|
5059
|
+
}
|
|
5060
|
+
if (endpoint.ref.layer !== undefined && endpoint.ref.layer !== 'values') {
|
|
5061
|
+
addDiagnostic('chart-target-state-layer-unsupported', 'warning', `${linkPath}.to`);
|
|
5062
|
+
return;
|
|
5063
|
+
}
|
|
5064
|
+
if (endpoint.ref.writable !== true) {
|
|
5065
|
+
addDiagnostic('chart-target-state-not-explicitly-writable', 'warning', `${linkPath}.to`);
|
|
5066
|
+
return;
|
|
5067
|
+
}
|
|
5068
|
+
if (intent !== 'state-write') {
|
|
5069
|
+
addDiagnostic('chart-target-state-intent-invalid', 'warning', `${linkPath}.intent`);
|
|
5070
|
+
return;
|
|
5071
|
+
}
|
|
5072
|
+
const schema = this.readDeclaredStateSchema(page.state);
|
|
5073
|
+
if (!schema || !Object.prototype.hasOwnProperty.call(schema, endpoint.ref.path)) {
|
|
5074
|
+
addDiagnostic('chart-target-state-path-undeclared', 'warning', `${linkPath}.to.ref.path`);
|
|
5075
|
+
return;
|
|
5076
|
+
}
|
|
5077
|
+
const id = endpoint.ref.path;
|
|
5078
|
+
this.upsertTarget(targets, `update-context:${id}`, {
|
|
5079
|
+
id,
|
|
5080
|
+
label: this.stateLabel(schema[id], id),
|
|
5081
|
+
actions: ['update-context'],
|
|
5082
|
+
events: [sourceEvent],
|
|
5083
|
+
}, sourceEvent);
|
|
5084
|
+
}
|
|
5085
|
+
sourceEvent(port) {
|
|
5086
|
+
switch (port.id) {
|
|
5087
|
+
case 'pointAction':
|
|
5088
|
+
return 'pointClick';
|
|
5089
|
+
case 'selectionChange':
|
|
5090
|
+
case 'drillDown':
|
|
5091
|
+
case 'crossFilter':
|
|
5092
|
+
return port.id;
|
|
5093
|
+
default:
|
|
5094
|
+
return null;
|
|
5095
|
+
}
|
|
5096
|
+
}
|
|
5097
|
+
isWidgetLinkSemanticallyCompatible(sourcePort, targetPort, transform) {
|
|
5098
|
+
let effectiveSourceKind = sourcePort.semanticKind;
|
|
5099
|
+
if (transform !== undefined) {
|
|
5100
|
+
if (!this.isRecord(transform) || !Array.isArray(transform['steps'])) {
|
|
5101
|
+
return false;
|
|
5102
|
+
}
|
|
5103
|
+
if (transform['steps'].some((step) => (!this.isRecord(step)
|
|
5104
|
+
|| typeof step['kind'] !== 'string'
|
|
5105
|
+
|| typeof step['phase'] !== 'string'))) {
|
|
5106
|
+
return false;
|
|
5107
|
+
}
|
|
5108
|
+
const output = transform['output'];
|
|
5109
|
+
if (output !== undefined) {
|
|
5110
|
+
if (!this.isRecord(output)) {
|
|
5111
|
+
return false;
|
|
5112
|
+
}
|
|
5113
|
+
if (output['semanticKind'] !== undefined
|
|
5114
|
+
&& typeof output['semanticKind'] !== 'string') {
|
|
5115
|
+
return false;
|
|
5116
|
+
}
|
|
5117
|
+
effectiveSourceKind = typeof output['semanticKind'] === 'string'
|
|
5118
|
+
? output['semanticKind']
|
|
5119
|
+
: effectiveSourceKind;
|
|
5120
|
+
}
|
|
5121
|
+
}
|
|
5122
|
+
return targetPort.semanticKind === 'query-context'
|
|
5123
|
+
&& (effectiveSourceKind === 'query-context' || effectiveSourceKind === 'value');
|
|
5124
|
+
}
|
|
5125
|
+
upsertTarget(targets, key, target, sourceEvent) {
|
|
5126
|
+
const current = targets.get(key);
|
|
5127
|
+
if (!current) {
|
|
5128
|
+
targets.set(key, target);
|
|
5129
|
+
return;
|
|
5130
|
+
}
|
|
5131
|
+
targets.set(key, {
|
|
5132
|
+
...current,
|
|
5133
|
+
events: current.events?.includes(sourceEvent)
|
|
5134
|
+
? current.events
|
|
5135
|
+
: [...(current.events ?? []), sourceEvent],
|
|
5136
|
+
});
|
|
5137
|
+
}
|
|
5138
|
+
isPageEvidence(value) {
|
|
5139
|
+
if (!this.isRecord(value) || !Array.isArray(value['widgets'])) {
|
|
5140
|
+
return false;
|
|
5141
|
+
}
|
|
5142
|
+
const composition = value['composition'];
|
|
5143
|
+
if (composition === undefined) {
|
|
5144
|
+
return true;
|
|
5145
|
+
}
|
|
5146
|
+
if (!this.isRecord(composition)) {
|
|
5147
|
+
return false;
|
|
5148
|
+
}
|
|
5149
|
+
return composition['links'] === undefined || Array.isArray(composition['links']);
|
|
5150
|
+
}
|
|
5151
|
+
isWidgetEvidence(value) {
|
|
5152
|
+
if (!this.isRecord(value) || typeof value['key'] !== 'string') {
|
|
5153
|
+
return false;
|
|
5154
|
+
}
|
|
5155
|
+
const definition = value['definition'];
|
|
5156
|
+
return this.isRecord(definition) && typeof definition['id'] === 'string';
|
|
5157
|
+
}
|
|
5158
|
+
sourceWidgetKey(value) {
|
|
5159
|
+
if (!this.isRecord(value)) {
|
|
5160
|
+
return undefined;
|
|
5161
|
+
}
|
|
5162
|
+
const source = value['from'];
|
|
5163
|
+
if (!this.hasEndpointKind(source, 'component-port') || !this.isRecord(source['ref'])) {
|
|
5164
|
+
return undefined;
|
|
5165
|
+
}
|
|
5166
|
+
return typeof source['ref']['widget'] === 'string'
|
|
5167
|
+
? source['ref']['widget']
|
|
5168
|
+
: undefined;
|
|
5169
|
+
}
|
|
5170
|
+
readComponentPortEndpoint(value) {
|
|
5171
|
+
if (!this.hasEndpointKind(value, 'component-port')) {
|
|
5172
|
+
return null;
|
|
5173
|
+
}
|
|
5174
|
+
const ref = value['ref'];
|
|
5175
|
+
if (!this.isRecord(ref)
|
|
5176
|
+
|| typeof ref['widget'] !== 'string'
|
|
5177
|
+
|| typeof ref['port'] !== 'string'
|
|
5178
|
+
|| typeof ref['direction'] !== 'string'
|
|
5179
|
+
|| (ref['componentType'] !== undefined && typeof ref['componentType'] !== 'string')
|
|
5180
|
+
|| (ref['nestedPath'] !== undefined && !Array.isArray(ref['nestedPath']))) {
|
|
5181
|
+
return null;
|
|
5182
|
+
}
|
|
5183
|
+
return {
|
|
5184
|
+
kind: 'component-port',
|
|
5185
|
+
ref: {
|
|
5186
|
+
widget: ref['widget'],
|
|
5187
|
+
port: ref['port'],
|
|
5188
|
+
direction: ref['direction'],
|
|
5189
|
+
...(typeof ref['componentType'] === 'string'
|
|
5190
|
+
? { componentType: ref['componentType'] }
|
|
5191
|
+
: {}),
|
|
5192
|
+
...(Array.isArray(ref['nestedPath'])
|
|
5193
|
+
? { nestedPath: ref['nestedPath'] }
|
|
5194
|
+
: {}),
|
|
5195
|
+
},
|
|
5196
|
+
};
|
|
5197
|
+
}
|
|
5198
|
+
readStateEndpoint(value) {
|
|
5199
|
+
if (!this.hasEndpointKind(value, 'state')) {
|
|
5200
|
+
return null;
|
|
5201
|
+
}
|
|
5202
|
+
const ref = value['ref'];
|
|
5203
|
+
if (!this.isRecord(ref)
|
|
5204
|
+
|| typeof ref['path'] !== 'string'
|
|
5205
|
+
|| (ref['layer'] !== undefined && typeof ref['layer'] !== 'string')
|
|
5206
|
+
|| (ref['writable'] !== undefined && typeof ref['writable'] !== 'boolean')) {
|
|
5207
|
+
return null;
|
|
5208
|
+
}
|
|
5209
|
+
return {
|
|
5210
|
+
kind: 'state',
|
|
5211
|
+
ref: {
|
|
5212
|
+
path: ref['path'],
|
|
5213
|
+
...(typeof ref['layer'] === 'string' ? { layer: ref['layer'] } : {}),
|
|
5214
|
+
...(typeof ref['writable'] === 'boolean' ? { writable: ref['writable'] } : {}),
|
|
5215
|
+
},
|
|
5216
|
+
};
|
|
5217
|
+
}
|
|
5218
|
+
readDeclaredStateSchema(value) {
|
|
5219
|
+
if (!this.isRecord(value) || !this.isRecord(value['schema'])) {
|
|
5220
|
+
return null;
|
|
5221
|
+
}
|
|
5222
|
+
return value['schema'];
|
|
5223
|
+
}
|
|
5224
|
+
isAuthorablePort(port, direction) {
|
|
5225
|
+
return !!port
|
|
5226
|
+
&& port.direction === direction
|
|
5227
|
+
&& port.exposure?.public === true
|
|
5228
|
+
&& port.exposure.deprecated !== true;
|
|
5229
|
+
}
|
|
5230
|
+
endpointComponentTypeMatches(endpointComponentType, ...canonicalIds) {
|
|
5231
|
+
if (endpointComponentType === undefined) {
|
|
5232
|
+
return true;
|
|
5233
|
+
}
|
|
5234
|
+
return canonicalIds.some((candidate) => candidate === endpointComponentType);
|
|
5235
|
+
}
|
|
5236
|
+
widgetLabel(friendlyName, widgetKey) {
|
|
5237
|
+
const label = friendlyName.trim();
|
|
5238
|
+
return label && label !== widgetKey
|
|
5239
|
+
? `${label} (${widgetKey})`
|
|
5240
|
+
: widgetKey;
|
|
5241
|
+
}
|
|
5242
|
+
stateLabel(descriptor, path) {
|
|
5243
|
+
const description = this.isRecord(descriptor) && typeof descriptor['description'] === 'string'
|
|
5244
|
+
? descriptor['description'].trim()
|
|
5245
|
+
: '';
|
|
5246
|
+
return description && description !== path
|
|
5247
|
+
? `${description} (${path})`
|
|
5248
|
+
: path;
|
|
5249
|
+
}
|
|
5250
|
+
isDeprecatedLink(value) {
|
|
5251
|
+
const metadata = value['metadata'];
|
|
5252
|
+
return this.isRecord(metadata) && metadata['deprecated'] === true;
|
|
5253
|
+
}
|
|
5254
|
+
hasEndpointKind(value, kind) {
|
|
5255
|
+
return this.isRecord(value) && value['kind'] === kind;
|
|
5256
|
+
}
|
|
5257
|
+
isRecord(value) {
|
|
5258
|
+
return !!value && typeof value === 'object' && !Array.isArray(value);
|
|
5259
|
+
}
|
|
5260
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: ChartCompositionTargetCatalogAdapter, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
5261
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: ChartCompositionTargetCatalogAdapter, providedIn: 'root' });
|
|
5262
|
+
}
|
|
5263
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: ChartCompositionTargetCatalogAdapter, decorators: [{
|
|
5264
|
+
type: Injectable,
|
|
5265
|
+
args: [{ providedIn: 'root' }]
|
|
5266
|
+
}] });
|
|
5267
|
+
|
|
5268
|
+
class ChartResourceCapabilityCatalogAdapter {
|
|
5269
|
+
toResourceOperations(capabilities) {
|
|
5270
|
+
if (!capabilities) {
|
|
5271
|
+
return [];
|
|
5272
|
+
}
|
|
5273
|
+
const operations = [];
|
|
5274
|
+
if (capabilities.canonicalOperations?.['statsGroupBy'] === true) {
|
|
5275
|
+
operations.push('group-by');
|
|
5276
|
+
}
|
|
5277
|
+
if (capabilities.canonicalOperations?.['statsTimeSeries'] === true) {
|
|
5278
|
+
operations.push('timeseries');
|
|
5279
|
+
}
|
|
5280
|
+
if (capabilities.canonicalOperations?.['statsDistribution'] === true) {
|
|
5281
|
+
operations.push('distribution');
|
|
5282
|
+
}
|
|
5283
|
+
return operations;
|
|
5284
|
+
}
|
|
5285
|
+
toFieldOptions(capabilities, operation) {
|
|
5286
|
+
if (!capabilities) {
|
|
5287
|
+
return [];
|
|
5288
|
+
}
|
|
5289
|
+
const fields = capabilities?.stats?.fields ?? [];
|
|
5290
|
+
const resourceOperations = this.toResourceOperations(capabilities);
|
|
5291
|
+
const fieldOptions = fields
|
|
5292
|
+
.filter((field) => !!field?.field)
|
|
5293
|
+
.flatMap((field) => this.toFieldOptionsForRoles(field, capabilities, resourceOperations));
|
|
5294
|
+
const countOption = this.toCountOption(fields, capabilities, resourceOperations);
|
|
5295
|
+
return [...fieldOptions, ...(countOption ? [countOption] : [])]
|
|
5296
|
+
.filter((option) => !operation || option.operations?.includes(operation));
|
|
5297
|
+
}
|
|
5298
|
+
toFieldOptionsForRoles(field, capabilities, resourceOperations) {
|
|
5299
|
+
const aggregations = this.resolveAggregations(field);
|
|
5300
|
+
const shared = {
|
|
5301
|
+
field: field.field,
|
|
5302
|
+
...(field.label ? { label: field.label } : {}),
|
|
5303
|
+
...(capabilities.resourceKey ? { resourceIds: [capabilities.resourceKey] } : {}),
|
|
5304
|
+
...(capabilities.resourcePath ? { resourcePaths: [capabilities.resourcePath] } : {}),
|
|
5305
|
+
...(aggregations.length ? { aggregations } : {}),
|
|
5306
|
+
};
|
|
5307
|
+
const options = [];
|
|
5308
|
+
const dimensionOperations = this.resolveDimensionOperations(field)
|
|
5309
|
+
.filter((candidate) => resourceOperations.includes(candidate));
|
|
5310
|
+
const distributionModes = this.resolveDistributionModes(field);
|
|
5311
|
+
if (dimensionOperations.length) {
|
|
5312
|
+
options.push({
|
|
5313
|
+
...shared,
|
|
5314
|
+
roles: field.timeSeriesEligible === true
|
|
5315
|
+
? ['dimension', 'time']
|
|
5316
|
+
: ['dimension'],
|
|
5317
|
+
operations: dimensionOperations,
|
|
5318
|
+
...(dimensionOperations.includes('distribution')
|
|
5319
|
+
? { distributionModes }
|
|
5320
|
+
: {}),
|
|
5321
|
+
});
|
|
5322
|
+
}
|
|
5323
|
+
if (field.metricFieldEligible === true && aggregations.length) {
|
|
5324
|
+
options.push({
|
|
5325
|
+
...shared,
|
|
5326
|
+
aggregable: true,
|
|
5327
|
+
roles: ['metric'],
|
|
5328
|
+
operations: resourceOperations,
|
|
5329
|
+
...(resourceOperations.includes('distribution')
|
|
5330
|
+
? { distributionModes: ['terms'] }
|
|
5331
|
+
: {}),
|
|
5332
|
+
});
|
|
5333
|
+
}
|
|
5334
|
+
return options;
|
|
5335
|
+
}
|
|
5336
|
+
resolveDimensionOperations(field) {
|
|
5337
|
+
const operations = new Set();
|
|
5338
|
+
const modes = field.modes ?? [];
|
|
5339
|
+
if (field.groupByEligible === true || modes.includes('GROUP_BY')) {
|
|
5340
|
+
operations.add('group-by');
|
|
5341
|
+
}
|
|
5342
|
+
if (field.timeSeriesEligible === true || modes.includes('TIME_SERIES')) {
|
|
5343
|
+
operations.add('timeseries');
|
|
5344
|
+
}
|
|
5345
|
+
if (field.distributionTermsEligible === true
|
|
5346
|
+
|| field.distributionHistogramEligible === true
|
|
5347
|
+
|| modes.includes('DISTRIBUTION_TERMS')
|
|
5348
|
+
|| modes.includes('DISTRIBUTION_HISTOGRAM')) {
|
|
5349
|
+
operations.add('distribution');
|
|
5350
|
+
}
|
|
5351
|
+
return Array.from(operations);
|
|
5352
|
+
}
|
|
5353
|
+
resolveAggregations(field) {
|
|
5354
|
+
return (field.metrics ?? [])
|
|
5355
|
+
.map((metric) => this.mapMetric(metric))
|
|
5356
|
+
.filter((metric) => !!metric && metric !== 'count');
|
|
5357
|
+
}
|
|
5358
|
+
resolveDistributionModes(field) {
|
|
5359
|
+
const modes = [];
|
|
5360
|
+
if (field.distributionTermsEligible === true || field.modes?.includes('DISTRIBUTION_TERMS')) {
|
|
5361
|
+
modes.push('terms');
|
|
5362
|
+
}
|
|
5363
|
+
if (field.distributionHistogramEligible === true
|
|
5364
|
+
|| field.modes?.includes('DISTRIBUTION_HISTOGRAM')) {
|
|
5365
|
+
modes.push('histogram');
|
|
5366
|
+
}
|
|
5367
|
+
return modes;
|
|
5368
|
+
}
|
|
5369
|
+
mapMetric(metric) {
|
|
5370
|
+
switch (String(metric || '').trim().toUpperCase()) {
|
|
5371
|
+
case 'COUNT':
|
|
5372
|
+
return 'count';
|
|
5373
|
+
case 'DISTINCT_COUNT':
|
|
5374
|
+
return 'distinct-count';
|
|
5375
|
+
case 'SUM':
|
|
5376
|
+
return 'sum';
|
|
5377
|
+
case 'AVG':
|
|
5378
|
+
case 'AVERAGE':
|
|
5379
|
+
return 'avg';
|
|
5380
|
+
case 'MIN':
|
|
5381
|
+
return 'min';
|
|
5382
|
+
case 'MAX':
|
|
5383
|
+
return 'max';
|
|
5384
|
+
default:
|
|
5385
|
+
return null;
|
|
5386
|
+
}
|
|
5387
|
+
}
|
|
5388
|
+
toCountOption(fields, capabilities, resourceOperations) {
|
|
5389
|
+
if (!resourceOperations.length
|
|
5390
|
+
|| !fields.some((field) => (field.metrics ?? []).some((metric) => String(metric).toUpperCase() === 'COUNT'))) {
|
|
5391
|
+
return null;
|
|
5392
|
+
}
|
|
5393
|
+
return {
|
|
5394
|
+
field: 'count',
|
|
5395
|
+
label: 'Count',
|
|
5396
|
+
aggregable: true,
|
|
5397
|
+
roles: ['metric'],
|
|
5398
|
+
...(capabilities.resourceKey ? { resourceIds: [capabilities.resourceKey] } : {}),
|
|
5399
|
+
...(capabilities.resourcePath ? { resourcePaths: [capabilities.resourcePath] } : {}),
|
|
5400
|
+
operations: resourceOperations,
|
|
5401
|
+
...(resourceOperations.includes('distribution')
|
|
5402
|
+
? { distributionModes: ['terms', 'histogram'] }
|
|
5403
|
+
: {}),
|
|
5404
|
+
aggregations: ['count'],
|
|
5405
|
+
};
|
|
5406
|
+
}
|
|
5407
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: ChartResourceCapabilityCatalogAdapter, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
5408
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: ChartResourceCapabilityCatalogAdapter, providedIn: 'root' });
|
|
5409
|
+
}
|
|
5410
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: ChartResourceCapabilityCatalogAdapter, decorators: [{
|
|
5411
|
+
type: Injectable,
|
|
5412
|
+
args: [{ providedIn: 'root' }]
|
|
5413
|
+
}] });
|
|
5414
|
+
|
|
5415
|
+
class PraxisChartConfigEditorContextResolverService {
|
|
5416
|
+
static REQUEST_TIMEOUT_MS = 10_000;
|
|
5417
|
+
discovery = inject(ResourceDiscoveryService);
|
|
5418
|
+
capabilityAdapter = inject(ChartResourceCapabilityCatalogAdapter);
|
|
5419
|
+
targetAdapter = inject(ChartCompositionTargetCatalogAdapter);
|
|
5420
|
+
/**
|
|
5421
|
+
* ComponentConfigEditorContextRequest currently exposes no principal, tenant, or
|
|
5422
|
+
* profile identity. Do not infer an authorization scope from widget/page data.
|
|
5423
|
+
* Successful reads are not cached across requests because capabilities may be
|
|
5424
|
+
* permission-sensitive. Only concurrent reads are deduplicated, and every pending
|
|
5425
|
+
* read has a finite timeout; server-side authorization remains canonical.
|
|
5426
|
+
*/
|
|
5427
|
+
catalogProjectionRequest;
|
|
5428
|
+
capabilityRequests = new Map();
|
|
5429
|
+
async resolve(request) {
|
|
5430
|
+
const targetProjection = this.targetAdapter.resolve(request);
|
|
5431
|
+
let projection;
|
|
5432
|
+
try {
|
|
5433
|
+
projection = await this.getCatalogProjection();
|
|
5434
|
+
}
|
|
5435
|
+
catch (error) {
|
|
5436
|
+
return {
|
|
5437
|
+
context: {
|
|
5438
|
+
availableResources: [],
|
|
5439
|
+
availableFields: [],
|
|
5440
|
+
availableTargets: targetProjection.targets,
|
|
5441
|
+
},
|
|
5442
|
+
diagnostics: [
|
|
5443
|
+
...(targetProjection.diagnostics ?? []),
|
|
5444
|
+
this.httpDiagnostic(error, 'catalog'),
|
|
5445
|
+
],
|
|
5446
|
+
};
|
|
5447
|
+
}
|
|
5448
|
+
const resources = projection.candidates.map((candidate) => candidate.option);
|
|
5449
|
+
const selectedResource = this.selectedResource(request.persistedInputs);
|
|
5450
|
+
if (!selectedResource) {
|
|
5451
|
+
return {
|
|
5452
|
+
context: {
|
|
5453
|
+
availableResources: resources,
|
|
5454
|
+
availableFields: [],
|
|
5455
|
+
availableTargets: targetProjection.targets,
|
|
5456
|
+
},
|
|
5457
|
+
diagnostics: [
|
|
5458
|
+
...projection.diagnostics,
|
|
5459
|
+
...(targetProjection.diagnostics ?? []),
|
|
5460
|
+
],
|
|
5461
|
+
};
|
|
5462
|
+
}
|
|
5463
|
+
const selected = await this.resolveResourceFromProjection(selectedResource, projection);
|
|
5464
|
+
const resolvedResources = selected.resource
|
|
5465
|
+
? resources.map((resource) => resource.id === selected.resource?.id ? selected.resource : resource)
|
|
5466
|
+
: resources;
|
|
5467
|
+
return {
|
|
5468
|
+
context: {
|
|
5469
|
+
availableResources: resolvedResources,
|
|
5470
|
+
availableFields: selected.fields,
|
|
5471
|
+
availableTargets: targetProjection.targets,
|
|
5472
|
+
},
|
|
5473
|
+
diagnostics: [
|
|
5474
|
+
...projection.diagnostics,
|
|
5475
|
+
...(targetProjection.diagnostics ?? []),
|
|
5476
|
+
...selected.diagnostics,
|
|
5477
|
+
],
|
|
5478
|
+
};
|
|
5479
|
+
}
|
|
5480
|
+
async resolveResource(resourceIdentity) {
|
|
5481
|
+
let projection;
|
|
5482
|
+
try {
|
|
5483
|
+
projection = await this.getCatalogProjection();
|
|
5484
|
+
}
|
|
5485
|
+
catch (error) {
|
|
5486
|
+
return {
|
|
5487
|
+
fields: [],
|
|
5488
|
+
diagnostics: [this.httpDiagnostic(error, 'catalog')],
|
|
5489
|
+
};
|
|
5490
|
+
}
|
|
5491
|
+
const selected = await this.resolveResourceFromProjection(resourceIdentity, projection);
|
|
5492
|
+
return {
|
|
5493
|
+
...selected,
|
|
5494
|
+
diagnostics: [...projection.diagnostics, ...selected.diagnostics],
|
|
5495
|
+
};
|
|
5496
|
+
}
|
|
5497
|
+
async resolveResourceFromProjection(resourceIdentity, projection) {
|
|
5498
|
+
const candidate = this.findCandidate(projection.candidates, resourceIdentity);
|
|
5499
|
+
if (!candidate) {
|
|
5500
|
+
return {
|
|
5501
|
+
fields: [],
|
|
5502
|
+
diagnostics: [{
|
|
5503
|
+
code: 'chart-resource-catalog-selection-stale',
|
|
5504
|
+
severity: 'error',
|
|
5505
|
+
path: 'chartDocument.source.resource',
|
|
5506
|
+
}],
|
|
5507
|
+
};
|
|
5508
|
+
}
|
|
5509
|
+
let capabilities;
|
|
5510
|
+
try {
|
|
5511
|
+
capabilities = await this.getCapabilities(candidate);
|
|
5512
|
+
}
|
|
5513
|
+
catch (error) {
|
|
5514
|
+
return {
|
|
5515
|
+
fields: [],
|
|
5516
|
+
diagnostics: [this.httpDiagnostic(error, 'capabilities')],
|
|
5517
|
+
};
|
|
5518
|
+
}
|
|
5519
|
+
if (String(capabilities.resourceKey || '').trim() !== candidate.option.id) {
|
|
5520
|
+
return {
|
|
5521
|
+
fields: [],
|
|
5522
|
+
diagnostics: [{
|
|
5523
|
+
code: 'chart-resource-capabilities-identity-mismatch',
|
|
5524
|
+
severity: 'error',
|
|
5525
|
+
path: 'chartDocument.source.resource',
|
|
5526
|
+
}],
|
|
5527
|
+
};
|
|
5528
|
+
}
|
|
5529
|
+
const capabilityResourcePath = String(capabilities.resourcePath || '').trim();
|
|
5530
|
+
if (!this.isGovernedRelativePath(capabilityResourcePath)) {
|
|
5531
|
+
return {
|
|
5532
|
+
fields: [],
|
|
5533
|
+
diagnostics: [{
|
|
5534
|
+
code: 'chart-resource-capabilities-path-invalid',
|
|
5535
|
+
severity: 'error',
|
|
5536
|
+
path: 'chartDocument.source.resource',
|
|
5537
|
+
}],
|
|
5538
|
+
};
|
|
5539
|
+
}
|
|
5540
|
+
const catalogResourcePath = this.normalizeOperationalPath(candidate.option.path);
|
|
5541
|
+
if (this.normalizeOperationalPath(capabilityResourcePath) !== catalogResourcePath) {
|
|
5542
|
+
return {
|
|
5543
|
+
fields: [],
|
|
5544
|
+
diagnostics: [{
|
|
5545
|
+
code: 'chart-resource-capabilities-path-mismatch',
|
|
5546
|
+
severity: 'error',
|
|
5547
|
+
path: 'chartDocument.source.resource',
|
|
5548
|
+
}],
|
|
5549
|
+
};
|
|
5550
|
+
}
|
|
5551
|
+
const canonicalCapabilities = {
|
|
5552
|
+
...capabilities,
|
|
5553
|
+
resourcePath: catalogResourcePath,
|
|
5554
|
+
};
|
|
5555
|
+
const operations = this.capabilityAdapter.toResourceOperations(canonicalCapabilities);
|
|
5556
|
+
const fields = this.capabilityAdapter.toFieldOptions(canonicalCapabilities);
|
|
5557
|
+
const diagnostics = [];
|
|
5558
|
+
if (!operations.length) {
|
|
5559
|
+
diagnostics.push({
|
|
5560
|
+
code: 'chart-resource-stats-operations-empty',
|
|
5561
|
+
severity: 'warning',
|
|
5562
|
+
path: 'chartDocument.source.operation',
|
|
5563
|
+
});
|
|
5564
|
+
}
|
|
5565
|
+
if (!fields.length) {
|
|
5566
|
+
diagnostics.push({
|
|
5567
|
+
code: 'chart-resource-stats-fields-empty',
|
|
5568
|
+
severity: 'error',
|
|
5569
|
+
path: 'chartDocument.dimensions',
|
|
5570
|
+
});
|
|
5571
|
+
}
|
|
5572
|
+
return {
|
|
5573
|
+
resource: {
|
|
5574
|
+
...candidate.option,
|
|
5575
|
+
id: canonicalCapabilities.resourceKey,
|
|
5576
|
+
path: catalogResourcePath,
|
|
5577
|
+
operations,
|
|
5578
|
+
},
|
|
5579
|
+
fields,
|
|
5580
|
+
diagnostics,
|
|
5581
|
+
};
|
|
5582
|
+
}
|
|
5583
|
+
async getCatalogProjection() {
|
|
5584
|
+
const cached = this.catalogProjectionRequest;
|
|
5585
|
+
if (cached) {
|
|
5586
|
+
return cached.promise;
|
|
5587
|
+
}
|
|
5588
|
+
let request;
|
|
5589
|
+
const promise = firstValueFrom(this.discovery.getSchemaCatalog().pipe(timeout(PraxisChartConfigEditorContextResolverService.REQUEST_TIMEOUT_MS)))
|
|
5590
|
+
.then((catalog) => this.projectCatalog(catalog))
|
|
5591
|
+
.finally(() => {
|
|
5592
|
+
if (this.catalogProjectionRequest === request) {
|
|
5593
|
+
this.catalogProjectionRequest = undefined;
|
|
5594
|
+
}
|
|
5595
|
+
});
|
|
5596
|
+
request = { promise };
|
|
5597
|
+
this.catalogProjectionRequest = request;
|
|
5598
|
+
return request.promise;
|
|
5599
|
+
}
|
|
5600
|
+
projectCatalog(catalog) {
|
|
5601
|
+
const byResourceKey = new Map();
|
|
5602
|
+
for (const endpoint of Array.isArray(catalog?.endpoints) ? catalog.endpoints : []) {
|
|
5603
|
+
const resourceKey = String(endpoint?.resourceKey || '').trim();
|
|
5604
|
+
if (!resourceKey) {
|
|
5605
|
+
continue;
|
|
5606
|
+
}
|
|
5607
|
+
byResourceKey.set(resourceKey, [...(byResourceKey.get(resourceKey) ?? []), endpoint]);
|
|
5608
|
+
}
|
|
5609
|
+
const candidates = [];
|
|
5610
|
+
const diagnostics = [];
|
|
5611
|
+
for (const [resourceKey, endpoints] of byResourceKey.entries()) {
|
|
5612
|
+
// The semantic resourceKey scopes the resource first. Path suffixes below only
|
|
5613
|
+
// identify canonical HTTP projections inside that governed scope; they never
|
|
5614
|
+
// route user intent or choose a resource from text.
|
|
5615
|
+
const declaredCapabilityEndpoints = endpoints.filter((endpoint) => (endpoint.method === 'GET'
|
|
5616
|
+
&& !endpoint.path.includes('{')
|
|
5617
|
+
&& endpoint.path.endsWith('/capabilities')));
|
|
5618
|
+
if (declaredCapabilityEndpoints.some((endpoint) => !this.isGovernedRelativePath(endpoint.path))) {
|
|
5619
|
+
diagnostics.push({
|
|
5620
|
+
code: 'chart-resource-catalog-path-invalid',
|
|
5621
|
+
severity: 'error',
|
|
5622
|
+
path: resourceKey,
|
|
5623
|
+
});
|
|
5624
|
+
continue;
|
|
5625
|
+
}
|
|
5626
|
+
const capabilityEndpoints = declaredCapabilityEndpoints.filter((endpoint) => (this.isGovernedRelativePath(endpoint.path.slice(0, -'/capabilities'.length))));
|
|
5627
|
+
if (declaredCapabilityEndpoints.length && !capabilityEndpoints.length) {
|
|
5628
|
+
diagnostics.push({
|
|
5629
|
+
code: 'chart-resource-catalog-path-invalid',
|
|
5630
|
+
severity: 'error',
|
|
5631
|
+
path: resourceKey,
|
|
5632
|
+
});
|
|
5633
|
+
continue;
|
|
5634
|
+
}
|
|
5635
|
+
if (capabilityEndpoints.length !== 1) {
|
|
5636
|
+
if (capabilityEndpoints.length > 1) {
|
|
5637
|
+
diagnostics.push({
|
|
5638
|
+
code: 'chart-resource-capabilities-endpoint-ambiguous',
|
|
5639
|
+
severity: 'warning',
|
|
5640
|
+
path: resourceKey,
|
|
5641
|
+
});
|
|
5642
|
+
}
|
|
5643
|
+
continue;
|
|
5644
|
+
}
|
|
5645
|
+
const capabilitiesEndpoint = capabilityEndpoints[0];
|
|
5646
|
+
const resourcePath = this.normalizeOperationalPath(capabilitiesEndpoint.path.slice(0, -'/capabilities'.length));
|
|
5647
|
+
const statsPaths = new Set([
|
|
5648
|
+
`${resourcePath}/stats/group-by`,
|
|
5649
|
+
`${resourcePath}/stats/timeseries`,
|
|
5650
|
+
`${resourcePath}/stats/distribution`,
|
|
5651
|
+
]);
|
|
5652
|
+
const publishesStats = endpoints.some((endpoint) => (endpoint.method === 'POST'
|
|
5653
|
+
&& statsPaths.has(endpoint.path)));
|
|
5654
|
+
if (!publishesStats) {
|
|
5655
|
+
continue;
|
|
5656
|
+
}
|
|
5657
|
+
const visual = capabilitiesEndpoint.resourceVisual
|
|
5658
|
+
?? endpoints.find((endpoint) => endpoint.resourceVisual)?.resourceVisual
|
|
5659
|
+
?? null;
|
|
5660
|
+
candidates.push({
|
|
5661
|
+
capabilitiesHref: capabilitiesEndpoint.path,
|
|
5662
|
+
option: {
|
|
5663
|
+
id: resourceKey,
|
|
5664
|
+
label: String(visual?.title || resourceKey).trim(),
|
|
5665
|
+
path: resourcePath,
|
|
5666
|
+
...(visual?.description ? { description: visual.description } : {}),
|
|
5667
|
+
},
|
|
5668
|
+
});
|
|
5669
|
+
}
|
|
5670
|
+
candidates.sort((left, right) => (left.option.label.localeCompare(right.option.label)
|
|
5671
|
+
|| left.option.id.localeCompare(right.option.id)));
|
|
5672
|
+
if (!candidates.length) {
|
|
5673
|
+
diagnostics.push({
|
|
5674
|
+
code: 'chart-resource-catalog-empty',
|
|
5675
|
+
severity: 'warning',
|
|
5676
|
+
path: 'availableResources',
|
|
5677
|
+
});
|
|
5678
|
+
}
|
|
5679
|
+
return { candidates, diagnostics };
|
|
5680
|
+
}
|
|
5681
|
+
async getCapabilities(candidate) {
|
|
5682
|
+
const key = `${candidate.option.id}\u0000${candidate.capabilitiesHref}`;
|
|
5683
|
+
const cached = this.capabilityRequests.get(key);
|
|
5684
|
+
if (cached) {
|
|
5685
|
+
return cached.promise;
|
|
5686
|
+
}
|
|
5687
|
+
let request;
|
|
5688
|
+
const promise = firstValueFrom(this.discovery.getCapabilitiesByUrl(candidate.capabilitiesHref).pipe(timeout(PraxisChartConfigEditorContextResolverService.REQUEST_TIMEOUT_MS)))
|
|
5689
|
+
.finally(() => {
|
|
5690
|
+
if (this.capabilityRequests.get(key) === request) {
|
|
5691
|
+
this.capabilityRequests.delete(key);
|
|
5692
|
+
}
|
|
5693
|
+
});
|
|
5694
|
+
request = { promise };
|
|
5695
|
+
this.capabilityRequests.set(key, request);
|
|
5696
|
+
return request.promise;
|
|
5697
|
+
}
|
|
5698
|
+
findCandidate(candidates, resourceIdentity) {
|
|
5699
|
+
const identity = String(resourceIdentity || '').trim();
|
|
5700
|
+
const normalizedPath = this.normalizeOperationalPath(identity);
|
|
5701
|
+
return candidates.find((candidate) => (candidate.option.id === identity
|
|
5702
|
+
|| this.normalizeOperationalPath(candidate.option.path) === normalizedPath));
|
|
5703
|
+
}
|
|
5704
|
+
selectedResource(inputs) {
|
|
5705
|
+
const chartDocument = this.asRecord(inputs?.['chartDocument']);
|
|
5706
|
+
const source = this.asRecord(chartDocument?.source);
|
|
5707
|
+
if (source?.['kind'] !== 'praxis.stats') {
|
|
5708
|
+
return null;
|
|
5709
|
+
}
|
|
5710
|
+
const resource = String(source?.['resource'] || '').trim();
|
|
5711
|
+
return resource || null;
|
|
5712
|
+
}
|
|
5713
|
+
normalizeOperationalPath(value) {
|
|
5714
|
+
const path = String(value || '').trim();
|
|
5715
|
+
let end = path.length;
|
|
5716
|
+
while (end > 1 && path.charAt(end - 1) === '/') {
|
|
5717
|
+
end -= 1;
|
|
5718
|
+
}
|
|
5719
|
+
return path.slice(0, end);
|
|
5720
|
+
}
|
|
5721
|
+
isGovernedRelativePath(value) {
|
|
5722
|
+
const path = String(value || '').trim();
|
|
5723
|
+
return path.startsWith('/') && !path.startsWith('//');
|
|
5724
|
+
}
|
|
5725
|
+
asRecord(value) {
|
|
5726
|
+
return value && typeof value === 'object' && !Array.isArray(value)
|
|
5727
|
+
? value
|
|
5728
|
+
: null;
|
|
5729
|
+
}
|
|
5730
|
+
httpDiagnostic(error, phase) {
|
|
5731
|
+
const denied = error instanceof HttpErrorResponse
|
|
5732
|
+
&& (error.status === 401 || error.status === 403);
|
|
5733
|
+
return {
|
|
5734
|
+
code: denied
|
|
5735
|
+
? `chart-resource-${phase}-denied`
|
|
5736
|
+
: `chart-resource-${phase}-unavailable`,
|
|
5737
|
+
severity: 'error',
|
|
5738
|
+
path: phase === 'catalog'
|
|
5739
|
+
? 'availableResources'
|
|
5740
|
+
: 'chartDocument.source.resource',
|
|
5741
|
+
};
|
|
5742
|
+
}
|
|
5743
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: PraxisChartConfigEditorContextResolverService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
5744
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: PraxisChartConfigEditorContextResolverService, providedIn: 'root' });
|
|
5745
|
+
}
|
|
5746
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: PraxisChartConfigEditorContextResolverService, decorators: [{
|
|
5747
|
+
type: Injectable,
|
|
5748
|
+
args: [{ providedIn: 'root' }]
|
|
5749
|
+
}] });
|
|
5750
|
+
|
|
4421
5751
|
class PraxisChartConfigEditor {
|
|
4422
5752
|
documentInput = input(null, { ...(ngDevMode ? { debugName: "documentInput" } : /* istanbul ignore next */ {}), alias: 'document' });
|
|
4423
5753
|
modeInput = input('edit', { ...(ngDevMode ? { debugName: "modeInput" } : /* istanbul ignore next */ {}), alias: 'mode' });
|
|
4424
5754
|
readonlyInput = input(false, { ...(ngDevMode ? { debugName: "readonlyInput" } : /* istanbul ignore next */ {}), alias: 'readonly' });
|
|
4425
|
-
availableResourcesInput = input(
|
|
4426
|
-
availableFieldsInput = input(
|
|
4427
|
-
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' });
|
|
4428
5758
|
apply = output();
|
|
4429
5759
|
save = output();
|
|
4430
5760
|
resetChange = output();
|
|
@@ -4474,22 +5804,38 @@ class PraxisChartConfigEditor {
|
|
|
4474
5804
|
normalizer = inject(ChartContractNormalizerService);
|
|
4475
5805
|
validator = inject(ChartContractValidationService);
|
|
4476
5806
|
previewMapper = inject(ChartEditorPreviewMapperService);
|
|
5807
|
+
contextResolver = inject(PraxisChartConfigEditorContextResolverService);
|
|
4477
5808
|
i18n = inject(PraxisI18nService);
|
|
4478
5809
|
currentDocument = signal(this.defaults.create(), ...(ngDevMode ? [{ debugName: "currentDocument" }] : /* istanbul ignore next */ []));
|
|
4479
5810
|
initialDocument = signal(this.defaults.create(), ...(ngDevMode ? [{ debugName: "initialDocument" }] : /* istanbul ignore next */ []));
|
|
5811
|
+
resolvedResources = signal(null, ...(ngDevMode ? [{ debugName: "resolvedResources" }] : /* istanbul ignore next */ []));
|
|
5812
|
+
resolvedFields = signal(null, ...(ngDevMode ? [{ debugName: "resolvedFields" }] : /* istanbul ignore next */ []));
|
|
5813
|
+
catalogResolutionDiagnostics = signal([], ...(ngDevMode ? [{ debugName: "catalogResolutionDiagnostics" }] : /* istanbul ignore next */ []));
|
|
4480
5814
|
lastExternalSignature = null;
|
|
5815
|
+
lastCatalogInputSignature = null;
|
|
5816
|
+
catalogResolutionRequestId = 0;
|
|
5817
|
+
baseAvailableResources = computed(() => (this.availableResourcesInput() ?? this.injectedData?.availableResources ?? []), ...(ngDevMode ? [{ debugName: "baseAvailableResources" }] : /* istanbul ignore next */ []));
|
|
5818
|
+
baseAvailableFields = computed(() => (this.availableFieldsInput() ?? this.injectedData?.availableFields ?? []), ...(ngDevMode ? [{ debugName: "baseAvailableFields" }] : /* istanbul ignore next */ []));
|
|
4481
5819
|
normalizedDocument = computed(() => this.normalizer.normalize(this.currentDocument()), ...(ngDevMode ? [{ debugName: "normalizedDocument" }] : /* istanbul ignore next */ []));
|
|
4482
|
-
validation = computed(() =>
|
|
5820
|
+
validation = computed(() => {
|
|
5821
|
+
const validation = this.validator.validate(this.normalizedDocument());
|
|
5822
|
+
const catalogIssues = this.catalogIssues();
|
|
5823
|
+
return {
|
|
5824
|
+
valid: validation.valid && !catalogIssues.some((issue) => issue.severity === 'error'),
|
|
5825
|
+
issues: [
|
|
5826
|
+
...validation.issues,
|
|
5827
|
+
...catalogIssues,
|
|
5828
|
+
],
|
|
5829
|
+
};
|
|
5830
|
+
}, ...(ngDevMode ? [{ debugName: "validation" }] : /* istanbul ignore next */ []));
|
|
4483
5831
|
issues = computed(() => this.validation().issues, ...(ngDevMode ? [{ debugName: "issues" }] : /* istanbul ignore next */ []));
|
|
4484
|
-
|
|
4485
|
-
|
|
4486
|
-
|
|
4487
|
-
|
|
4488
|
-
|
|
4489
|
-
|
|
4490
|
-
availableTargets = computed(() => this.availableTargetsInput().
|
|
4491
|
-
? this.availableTargetsInput()
|
|
4492
|
-
: (this.injectedData?.availableTargets ?? []), ...(ngDevMode ? [{ debugName: "availableTargets" }] : /* istanbul ignore next */ []));
|
|
5832
|
+
catalogIssues = computed(() => [
|
|
5833
|
+
...this.validateCatalogContext(),
|
|
5834
|
+
...this.catalogResolutionDiagnostics().map((diagnostic) => this.toCatalogIssue(diagnostic)),
|
|
5835
|
+
], ...(ngDevMode ? [{ debugName: "catalogIssues" }] : /* istanbul ignore next */ []));
|
|
5836
|
+
availableResources = computed(() => this.resolvedResources() ?? this.baseAvailableResources(), ...(ngDevMode ? [{ debugName: "availableResources" }] : /* istanbul ignore next */ []));
|
|
5837
|
+
availableFields = computed(() => this.resolvedFields() ?? this.baseAvailableFields(), ...(ngDevMode ? [{ debugName: "availableFields" }] : /* istanbul ignore next */ []));
|
|
5838
|
+
availableTargets = computed(() => (this.availableTargetsInput() ?? this.injectedData?.availableTargets ?? []), ...(ngDevMode ? [{ debugName: "availableTargets" }] : /* istanbul ignore next */ []));
|
|
4493
5839
|
preview = computed(() => {
|
|
4494
5840
|
if (!this.validation().valid) {
|
|
4495
5841
|
return null;
|
|
@@ -4497,6 +5843,21 @@ class PraxisChartConfigEditor {
|
|
|
4497
5843
|
return this.previewMapper.build(this.normalizedDocument());
|
|
4498
5844
|
}, ...(ngDevMode ? [{ debugName: "preview" }] : /* istanbul ignore next */ []));
|
|
4499
5845
|
constructor() {
|
|
5846
|
+
effect(() => {
|
|
5847
|
+
const signature = JSON.stringify({
|
|
5848
|
+
resources: this.baseAvailableResources(),
|
|
5849
|
+
fields: this.baseAvailableFields(),
|
|
5850
|
+
});
|
|
5851
|
+
if (signature === this.lastCatalogInputSignature) {
|
|
5852
|
+
return;
|
|
5853
|
+
}
|
|
5854
|
+
this.lastCatalogInputSignature = signature;
|
|
5855
|
+
this.catalogResolutionRequestId += 1;
|
|
5856
|
+
this.resolvedResources.set(null);
|
|
5857
|
+
this.resolvedFields.set(null);
|
|
5858
|
+
this.catalogResolutionDiagnostics.set([]);
|
|
5859
|
+
this.isBusy$.next(false);
|
|
5860
|
+
});
|
|
4500
5861
|
effect(() => {
|
|
4501
5862
|
const externalDocument = this.documentInput()
|
|
4502
5863
|
?? this.injectedData?.chartDocument
|
|
@@ -4515,6 +5876,29 @@ class PraxisChartConfigEditor {
|
|
|
4515
5876
|
this.lastExternalSignature = signature;
|
|
4516
5877
|
this.refreshState(false);
|
|
4517
5878
|
});
|
|
5879
|
+
effect(() => {
|
|
5880
|
+
const document = this.currentDocument();
|
|
5881
|
+
if (this.isReadonly() || document.source.kind !== 'praxis.stats') {
|
|
5882
|
+
return;
|
|
5883
|
+
}
|
|
5884
|
+
const resource = String(document.source.resource || '').trim();
|
|
5885
|
+
const option = resource
|
|
5886
|
+
? this.availableResources().find((candidate) => this.resourceIdentityMatches(candidate, resource))
|
|
5887
|
+
: undefined;
|
|
5888
|
+
const canonicalPath = String(option?.path || '').trim();
|
|
5889
|
+
if (!canonicalPath || canonicalPath === resource) {
|
|
5890
|
+
return;
|
|
5891
|
+
}
|
|
5892
|
+
this.patchDocument((current) => current.source.kind === 'praxis.stats'
|
|
5893
|
+
? {
|
|
5894
|
+
...current,
|
|
5895
|
+
source: {
|
|
5896
|
+
...current.source,
|
|
5897
|
+
resource: canonicalPath,
|
|
5898
|
+
},
|
|
5899
|
+
}
|
|
5900
|
+
: current);
|
|
5901
|
+
});
|
|
4518
5902
|
}
|
|
4519
5903
|
getSettingsValue() {
|
|
4520
5904
|
return structuredClone(this.normalizedDocument());
|
|
@@ -4523,6 +5907,11 @@ class PraxisChartConfigEditor {
|
|
|
4523
5907
|
return this.saveChanges().document;
|
|
4524
5908
|
}
|
|
4525
5909
|
reset() {
|
|
5910
|
+
this.catalogResolutionRequestId += 1;
|
|
5911
|
+
this.resolvedResources.set(null);
|
|
5912
|
+
this.resolvedFields.set(null);
|
|
5913
|
+
this.catalogResolutionDiagnostics.set([]);
|
|
5914
|
+
this.isBusy$.next(false);
|
|
4526
5915
|
const snapshot = structuredClone(this.initialDocument());
|
|
4527
5916
|
this.currentDocument.set(snapshot);
|
|
4528
5917
|
this.refreshState(false);
|
|
@@ -4640,15 +6029,29 @@ class PraxisChartConfigEditor {
|
|
|
4640
6029
|
}));
|
|
4641
6030
|
}
|
|
4642
6031
|
setResource(value) {
|
|
6032
|
+
const identity = value.trim();
|
|
6033
|
+
const option = this.resourceOptions().find((candidate) => (this.resourceIdentityMatches(candidate, identity)));
|
|
6034
|
+
const resource = String(option?.path || identity).trim();
|
|
4643
6035
|
this.patchDocument((document) => ({
|
|
4644
6036
|
...document,
|
|
4645
6037
|
source: document.source.kind === 'praxis.stats'
|
|
4646
6038
|
? {
|
|
4647
6039
|
...document.source,
|
|
4648
|
-
resource:
|
|
6040
|
+
resource: resource || undefined,
|
|
4649
6041
|
}
|
|
4650
6042
|
: document.source,
|
|
4651
6043
|
}));
|
|
6044
|
+
const baseCatalogAlreadyCoversSelection = !!option?.operations?.length
|
|
6045
|
+
&& this.baseAvailableFields().some((field) => this.fieldMatchesResourceOption(field, option));
|
|
6046
|
+
if (!resource || !option || baseCatalogAlreadyCoversSelection) {
|
|
6047
|
+
this.catalogResolutionRequestId += 1;
|
|
6048
|
+
this.isBusy$.next(false);
|
|
6049
|
+
this.catalogResolutionDiagnostics.set([]);
|
|
6050
|
+
this.resolvedFields.set(null);
|
|
6051
|
+
this.refreshState(this.isDirty$.value);
|
|
6052
|
+
return;
|
|
6053
|
+
}
|
|
6054
|
+
void this.resolveSelectedResource(resource);
|
|
4652
6055
|
}
|
|
4653
6056
|
setOperation(value) {
|
|
4654
6057
|
this.patchDocument((document) => ({
|
|
@@ -4695,10 +6098,18 @@ class PraxisChartConfigEditor {
|
|
|
4695
6098
|
source: document.source.kind === 'praxis.stats'
|
|
4696
6099
|
? {
|
|
4697
6100
|
...document.source,
|
|
4698
|
-
options:
|
|
4699
|
-
|
|
4700
|
-
|
|
4701
|
-
|
|
6101
|
+
options: value === 'terms'
|
|
6102
|
+
? {
|
|
6103
|
+
granularity: document.source.options?.granularity,
|
|
6104
|
+
fillGaps: document.source.options?.fillGaps,
|
|
6105
|
+
mode: value,
|
|
6106
|
+
orderBy: document.source.options?.orderBy,
|
|
6107
|
+
limit: document.source.options?.limit,
|
|
6108
|
+
}
|
|
6109
|
+
: {
|
|
6110
|
+
...(document.source.options ?? {}),
|
|
6111
|
+
mode: value,
|
|
6112
|
+
},
|
|
4702
6113
|
}
|
|
4703
6114
|
: document.source,
|
|
4704
6115
|
}));
|
|
@@ -4967,6 +6378,29 @@ class PraxisChartConfigEditor {
|
|
|
4967
6378
|
resourceOptions() {
|
|
4968
6379
|
return this.availableResources();
|
|
4969
6380
|
}
|
|
6381
|
+
selectedResourceOption() {
|
|
6382
|
+
const resource = this.resourceValue();
|
|
6383
|
+
if (!resource) {
|
|
6384
|
+
return undefined;
|
|
6385
|
+
}
|
|
6386
|
+
return this.resourceOptions().find((option) => this.resourceIdentityMatches(option, resource));
|
|
6387
|
+
}
|
|
6388
|
+
operationOptions() {
|
|
6389
|
+
const selectedResource = this.selectedResourceOption();
|
|
6390
|
+
if (!selectedResource) {
|
|
6391
|
+
return this.resourceOptions().length ? this.operations : [];
|
|
6392
|
+
}
|
|
6393
|
+
return selectedResource.operations?.length ? selectedResource.operations : [];
|
|
6394
|
+
}
|
|
6395
|
+
resourceCatalogUnavailable() {
|
|
6396
|
+
return this.doc().source.kind === 'praxis.stats' && !this.resourceOptions().length;
|
|
6397
|
+
}
|
|
6398
|
+
operationCatalogUnavailable() {
|
|
6399
|
+
return this.doc().source.kind === 'praxis.stats'
|
|
6400
|
+
&& !!this.resourceValue()
|
|
6401
|
+
&& !!this.selectedResourceOption()
|
|
6402
|
+
&& !this.operationOptions().length;
|
|
6403
|
+
}
|
|
4970
6404
|
granularityValue() {
|
|
4971
6405
|
return this.doc().source.kind === 'praxis.stats'
|
|
4972
6406
|
? (this.doc().source.options?.granularity ?? 'day')
|
|
@@ -5060,8 +6494,15 @@ class PraxisChartConfigEditor {
|
|
|
5060
6494
|
eventTarget(eventKey) {
|
|
5061
6495
|
return this.doc().events?.[eventKey]?.target ?? '';
|
|
5062
6496
|
}
|
|
5063
|
-
targetOptions() {
|
|
5064
|
-
|
|
6497
|
+
targetOptions(action, eventKey) {
|
|
6498
|
+
const targets = this.availableTargets();
|
|
6499
|
+
return targets.filter((target) => ((!action || !target.actions?.length || target.actions.includes(action))
|
|
6500
|
+
&& (!eventKey
|
|
6501
|
+
|| target.events === undefined
|
|
6502
|
+
|| target.events.includes(eventKey))));
|
|
6503
|
+
}
|
|
6504
|
+
targetCatalogUnavailable(action, eventKey) {
|
|
6505
|
+
return !!action && action !== 'emit' && !this.targetOptions(action, eventKey).length;
|
|
5065
6506
|
}
|
|
5066
6507
|
eventMappingText(eventKey) {
|
|
5067
6508
|
const mapping = this.doc().events?.[eventKey]?.mapping;
|
|
@@ -5083,12 +6524,23 @@ class PraxisChartConfigEditor {
|
|
|
5083
6524
|
if (!fields.length) {
|
|
5084
6525
|
return [];
|
|
5085
6526
|
}
|
|
5086
|
-
return fields.filter((field) =>
|
|
5087
|
-
|
|
5088
|
-
|
|
5089
|
-
|
|
5090
|
-
|
|
5091
|
-
|
|
6527
|
+
return fields.filter((field) => this.fieldMatchesSelectedResource(field)
|
|
6528
|
+
&& this.fieldMatchesOperation(field)
|
|
6529
|
+
&& this.fieldMatchesDistributionMode(field)
|
|
6530
|
+
&& this.fieldMatchesRole(field, role));
|
|
6531
|
+
}
|
|
6532
|
+
metricAggregationOptions(fieldName) {
|
|
6533
|
+
const field = this.fieldOptions('metric').find((option) => option.field === fieldName);
|
|
6534
|
+
if (!field?.aggregations?.length) {
|
|
6535
|
+
return this.metricAggregations;
|
|
6536
|
+
}
|
|
6537
|
+
return this.metricAggregations.filter((aggregation) => field.aggregations?.includes(aggregation));
|
|
6538
|
+
}
|
|
6539
|
+
fieldOptionLabel(field) {
|
|
6540
|
+
if (field.field === 'count') {
|
|
6541
|
+
return this.t('praxis.charts.editor.metricAggregation.count', 'count');
|
|
6542
|
+
}
|
|
6543
|
+
return field.label || field.field;
|
|
5092
6544
|
}
|
|
5093
6545
|
showMetricAxisControls() {
|
|
5094
6546
|
return this.doc().kind === 'combo';
|
|
@@ -5163,6 +6615,184 @@ class PraxisChartConfigEditor {
|
|
|
5163
6615
|
return accumulator;
|
|
5164
6616
|
}, {});
|
|
5165
6617
|
}
|
|
6618
|
+
validateCatalogContext() {
|
|
6619
|
+
const document = this.normalizedDocument();
|
|
6620
|
+
const issues = [];
|
|
6621
|
+
if (document.source.kind === 'praxis.stats') {
|
|
6622
|
+
if (!this.resourceOptions().length) {
|
|
6623
|
+
issues.push(this.catalogError('resource-catalog-missing', 'source.resource', this.t('praxis.charts.editor.catalog.resourceMissing', 'Resource catalog is required for governed praxis.stats authoring.')));
|
|
6624
|
+
}
|
|
6625
|
+
else if (document.source.resource && !this.selectedResourceOption()) {
|
|
6626
|
+
issues.push(this.catalogError('resource-stale', 'source.resource', this.t('praxis.charts.editor.catalog.resourceStale', 'Selected resource is not available in the governed resource catalog.')));
|
|
6627
|
+
}
|
|
6628
|
+
if (document.source.resource && this.selectedResourceOption() && !this.operationOptions().length) {
|
|
6629
|
+
issues.push(this.catalogError('operation-catalog-missing', 'source.operation', this.t('praxis.charts.editor.catalog.operationMissing', 'Selected resource does not publish authorable stats operations.')));
|
|
6630
|
+
}
|
|
6631
|
+
else if (document.source.operation
|
|
6632
|
+
&& this.operationOptions().length
|
|
6633
|
+
&& !this.operationOptions().includes(document.source.operation)) {
|
|
6634
|
+
issues.push(this.catalogError('operation-stale', 'source.operation', this.t('praxis.charts.editor.catalog.operationStale', 'Selected operation is not supported by the governed resource catalog.')));
|
|
6635
|
+
}
|
|
6636
|
+
if (document.source.resource
|
|
6637
|
+
&& this.selectedResourceOption()
|
|
6638
|
+
&& this.operationOptions().length
|
|
6639
|
+
&& !this.availableFields().length) {
|
|
6640
|
+
issues.push(this.catalogError('field-catalog-missing', 'dimensions', this.t('praxis.charts.editor.catalog.fieldCatalogMissing', 'The selected resource does not publish governed fields for chart authoring.')));
|
|
6641
|
+
}
|
|
6642
|
+
}
|
|
6643
|
+
if (this.availableFields().length) {
|
|
6644
|
+
document.dimensions?.forEach((dimension, index) => {
|
|
6645
|
+
if (!dimension.field || this.fieldOptions('dimension').some((field) => field.field === dimension.field)) {
|
|
6646
|
+
return;
|
|
6647
|
+
}
|
|
6648
|
+
issues.push(this.catalogError('dimension-field-stale', `dimensions[${index}].field`, this.t('praxis.charts.editor.catalog.dimensionFieldStale', 'Selected dimension is not eligible for the current resource and operation.')));
|
|
6649
|
+
});
|
|
6650
|
+
document.metrics?.forEach((metric, index) => {
|
|
6651
|
+
const metricField = metric.field;
|
|
6652
|
+
if (metricField && !this.fieldOptions('metric').some((field) => field.field === metricField)) {
|
|
6653
|
+
issues.push(this.catalogError('metric-field-stale', `metrics[${index}].field`, this.t('praxis.charts.editor.catalog.metricFieldStale', 'Selected metric is not eligible for the current resource and operation.')));
|
|
6654
|
+
return;
|
|
6655
|
+
}
|
|
6656
|
+
const aggregationOptions = this.metricAggregationOptions(metricField);
|
|
6657
|
+
if (metric.aggregation && !aggregationOptions.includes(metric.aggregation)) {
|
|
6658
|
+
issues.push(this.catalogError('metric-aggregation-stale', `metrics[${index}].aggregation`, this.t('praxis.charts.editor.catalog.metricAggregationStale', 'Selected aggregation is not allowed for the selected metric field.')));
|
|
6659
|
+
}
|
|
6660
|
+
});
|
|
6661
|
+
}
|
|
6662
|
+
for (const eventKey of ['pointClick', 'selectionChange', 'drillDown', 'crossFilter']) {
|
|
6663
|
+
const eventAction = document.events?.[eventKey];
|
|
6664
|
+
if (!eventAction?.action || eventAction.action === 'emit') {
|
|
6665
|
+
continue;
|
|
6666
|
+
}
|
|
6667
|
+
const targetOptions = this.targetOptions(eventAction.action, eventKey);
|
|
6668
|
+
if (!targetOptions.length) {
|
|
6669
|
+
issues.push(this.catalogError('target-catalog-missing', `events.${eventKey}.target`, this.t('praxis.charts.editor.catalog.targetMissing', 'Target catalog is required for this governed event action.')));
|
|
6670
|
+
}
|
|
6671
|
+
else if (eventAction.target && !targetOptions.some((target) => target.id === eventAction.target)) {
|
|
6672
|
+
issues.push(this.catalogError('target-stale', `events.${eventKey}.target`, this.t('praxis.charts.editor.catalog.targetStale', 'Selected target is not available for this event action.')));
|
|
6673
|
+
}
|
|
6674
|
+
}
|
|
6675
|
+
return issues;
|
|
6676
|
+
}
|
|
6677
|
+
fieldMatchesSelectedResource(field) {
|
|
6678
|
+
const resource = this.selectedResourceOption();
|
|
6679
|
+
if (!resource) {
|
|
6680
|
+
return true;
|
|
6681
|
+
}
|
|
6682
|
+
return this.fieldMatchesResourceOption(field, resource);
|
|
6683
|
+
}
|
|
6684
|
+
fieldMatchesResourceOption(field, resource) {
|
|
6685
|
+
const resourceIds = field.resourceIds ?? [];
|
|
6686
|
+
const resourcePaths = field.resourcePaths ?? [];
|
|
6687
|
+
if (!resourceIds.length && !resourcePaths.length) {
|
|
6688
|
+
return true;
|
|
6689
|
+
}
|
|
6690
|
+
return resourceIds.includes(resource.id)
|
|
6691
|
+
|| resourcePaths.some((path) => this.normalizeResourcePath(path) === this.normalizeResourcePath(resource.path));
|
|
6692
|
+
}
|
|
6693
|
+
async resolveSelectedResource(resource) {
|
|
6694
|
+
const requestId = ++this.catalogResolutionRequestId;
|
|
6695
|
+
this.resolvedFields.set([]);
|
|
6696
|
+
this.catalogResolutionDiagnostics.set([]);
|
|
6697
|
+
this.isBusy$.next(true);
|
|
6698
|
+
this.refreshState(this.isDirty$.value);
|
|
6699
|
+
try {
|
|
6700
|
+
const resolution = await this.contextResolver.resolveResource(resource);
|
|
6701
|
+
if (requestId !== this.catalogResolutionRequestId) {
|
|
6702
|
+
return;
|
|
6703
|
+
}
|
|
6704
|
+
if (resolution.resource) {
|
|
6705
|
+
const currentResources = this.resolvedResources() ?? this.baseAvailableResources();
|
|
6706
|
+
const matchIndex = currentResources.findIndex((candidate) => (candidate.id === resolution.resource?.id
|
|
6707
|
+
|| this.resourceIdentityMatches(candidate, resource)));
|
|
6708
|
+
this.resolvedResources.set(matchIndex >= 0
|
|
6709
|
+
? currentResources.map((candidate, index) => index === matchIndex ? resolution.resource : candidate)
|
|
6710
|
+
: [...currentResources, resolution.resource]);
|
|
6711
|
+
}
|
|
6712
|
+
this.resolvedFields.set(resolution.fields);
|
|
6713
|
+
this.catalogResolutionDiagnostics.set(resolution.diagnostics);
|
|
6714
|
+
}
|
|
6715
|
+
catch {
|
|
6716
|
+
if (requestId !== this.catalogResolutionRequestId) {
|
|
6717
|
+
return;
|
|
6718
|
+
}
|
|
6719
|
+
this.resolvedFields.set([]);
|
|
6720
|
+
this.catalogResolutionDiagnostics.set([{
|
|
6721
|
+
code: 'chart-resource-capabilities-unavailable',
|
|
6722
|
+
severity: 'error',
|
|
6723
|
+
path: 'chartDocument.source.resource',
|
|
6724
|
+
}]);
|
|
6725
|
+
}
|
|
6726
|
+
finally {
|
|
6727
|
+
if (requestId === this.catalogResolutionRequestId) {
|
|
6728
|
+
this.isBusy$.next(false);
|
|
6729
|
+
this.refreshState(this.isDirty$.value);
|
|
6730
|
+
}
|
|
6731
|
+
}
|
|
6732
|
+
}
|
|
6733
|
+
resourceIdentityMatches(option, identity) {
|
|
6734
|
+
const normalizedIdentity = this.normalizeResourcePath(identity);
|
|
6735
|
+
return option.id === identity
|
|
6736
|
+
|| this.normalizeResourcePath(option.path) === normalizedIdentity;
|
|
6737
|
+
}
|
|
6738
|
+
normalizeResourcePath(value) {
|
|
6739
|
+
return String(value || '').trim().replace(/^\/+|\/+$/g, '');
|
|
6740
|
+
}
|
|
6741
|
+
toCatalogIssue(diagnostic) {
|
|
6742
|
+
return {
|
|
6743
|
+
severity: diagnostic.severity === 'error' ? 'error' : 'warning',
|
|
6744
|
+
code: diagnostic.code,
|
|
6745
|
+
field: diagnostic.path || 'catalog',
|
|
6746
|
+
message: this.catalogDiagnosticMessage(diagnostic.code),
|
|
6747
|
+
};
|
|
6748
|
+
}
|
|
6749
|
+
catalogDiagnosticMessage(code) {
|
|
6750
|
+
switch (code) {
|
|
6751
|
+
case 'chart-resource-capabilities-denied':
|
|
6752
|
+
return this.t('praxis.charts.editor.catalog.capabilitiesDenied', 'Access to the selected resource capabilities was denied.');
|
|
6753
|
+
case 'chart-resource-capabilities-unavailable':
|
|
6754
|
+
return this.t('praxis.charts.editor.catalog.capabilitiesUnavailable', 'The selected resource capabilities are temporarily unavailable.');
|
|
6755
|
+
case 'chart-resource-catalog-selection-stale':
|
|
6756
|
+
return this.t('praxis.charts.editor.catalog.resourceStale', 'Selected resource is not available in the governed resource catalog.');
|
|
6757
|
+
case 'chart-resource-stats-fields-empty':
|
|
6758
|
+
return this.t('praxis.charts.editor.catalog.fieldCatalogMissing', 'The selected resource does not publish governed fields for chart authoring.');
|
|
6759
|
+
case 'chart-resource-stats-operations-empty':
|
|
6760
|
+
return this.t('praxis.charts.editor.catalog.operationMissing', 'Selected resource does not publish authorable stats operations.');
|
|
6761
|
+
default:
|
|
6762
|
+
return this.t('praxis.charts.editor.catalog.contextUnavailable', 'Governed chart authoring context could not be resolved.');
|
|
6763
|
+
}
|
|
6764
|
+
}
|
|
6765
|
+
fieldMatchesOperation(field) {
|
|
6766
|
+
const operation = this.doc().source.kind === 'praxis.stats' ? this.doc().source.operation : undefined;
|
|
6767
|
+
if (!operation || !field.operations?.length) {
|
|
6768
|
+
return true;
|
|
6769
|
+
}
|
|
6770
|
+
return field.operations.includes(operation);
|
|
6771
|
+
}
|
|
6772
|
+
fieldMatchesDistributionMode(field) {
|
|
6773
|
+
const source = this.doc().source;
|
|
6774
|
+
if (source.kind !== 'praxis.stats' || source.operation !== 'distribution') {
|
|
6775
|
+
return true;
|
|
6776
|
+
}
|
|
6777
|
+
if (!field.distributionModes?.length) {
|
|
6778
|
+
return true;
|
|
6779
|
+
}
|
|
6780
|
+
return field.distributionModes.includes(source.options?.mode ?? 'terms');
|
|
6781
|
+
}
|
|
6782
|
+
fieldMatchesRole(field, role) {
|
|
6783
|
+
if (!field.roles?.length) {
|
|
6784
|
+
return role === 'dimension' ? field.aggregable !== true : true;
|
|
6785
|
+
}
|
|
6786
|
+
return field.roles.includes(role);
|
|
6787
|
+
}
|
|
6788
|
+
catalogError(code, field, message) {
|
|
6789
|
+
return {
|
|
6790
|
+
severity: 'error',
|
|
6791
|
+
code,
|
|
6792
|
+
field,
|
|
6793
|
+
message,
|
|
6794
|
+
};
|
|
6795
|
+
}
|
|
5166
6796
|
normalizeSizingSizeInput(value) {
|
|
5167
6797
|
const trimmed = value.trim();
|
|
5168
6798
|
if (!trimmed) {
|
|
@@ -5206,7 +6836,7 @@ class PraxisChartConfigEditor {
|
|
|
5206
6836
|
this.documentChange.emit(structuredClone(this.normalizedDocument()));
|
|
5207
6837
|
}
|
|
5208
6838
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: PraxisChartConfigEditor, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
5209
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.14", type: PraxisChartConfigEditor, isStandalone: true, selector: "praxis-chart-config-editor", inputs: { documentInput: { classPropertyName: "documentInput", publicName: "document", isSignal: true, isRequired: false, transformFunction: null }, modeInput: { classPropertyName: "modeInput", publicName: "mode", isSignal: true, isRequired: false, transformFunction: null }, readonlyInput: { classPropertyName: "readonlyInput", publicName: "readonly", isSignal: true, isRequired: false, transformFunction: null }, availableResourcesInput: { classPropertyName: "availableResourcesInput", publicName: "availableResources", isSignal: true, isRequired: false, transformFunction: null }, availableFieldsInput: { classPropertyName: "availableFieldsInput", publicName: "availableFields", isSignal: true, isRequired: false, transformFunction: null }, availableTargetsInput: { classPropertyName: "availableTargetsInput", publicName: "availableTargets", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { apply: "apply", save: "save", resetChange: "resetChange", documentChange: "documentChange" }, providers: [providePraxisChartsI18n()], ngImport: i0, template: "<div class=\"editor-shell\">\n <div class=\"editor-nav\">\n @for (section of sections; track section.id) {\n <button\n mat-stroked-button\n type=\"button\"\n [class.active]=\"activeSection() === section.id\"\n (click)=\"setSection(section.id)\"\n >\n {{ t(section.labelKey, section.fallback) }}\n </button>\n }\n </div>\n\n <div class=\"editor-layout\">\n <div class=\"editor-form\">\n <mat-card class=\"editor-card\">\n <mat-card-content>\n @switch (activeSection()) {\n @case ('general') {\n <div class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.chartId', 'Chart ID') }}</mat-label>\n <input matInput [ngModel]=\"doc().chartId || ''\" (ngModelChange)=\"setChartId($event)\" [disabled]=\"isReadonly()\" />\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.kind', 'Kind') }}</mat-label>\n <mat-select [ngModel]=\"doc().kind\" (ngModelChange)=\"setKind($event)\" [disabled]=\"isReadonly()\">\n @for (kind of chartKinds; track kind) {\n <mat-option [value]=\"kind\">\n {{ t('praxis.charts.editor.kind.' + kind, kind) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.title', 'Title') }}</mat-label>\n <input matInput data-testid=\"chart-editor-title-input\" [ngModel]=\"titleValue()\" (ngModelChange)=\"setTitle($event)\" [disabled]=\"isReadonly()\" />\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.subtitle', 'Subtitle') }}</mat-label>\n <input matInput [ngModel]=\"subtitleValue()\" (ngModelChange)=\"setSubtitle($event)\" [disabled]=\"isReadonly()\" />\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.sizingMode', 'Sizing') }}</mat-label>\n <mat-select [ngModel]=\"sizingModeValue()\" (ngModelChange)=\"setSizingMode($event)\" [disabled]=\"isReadonly()\">\n @for (mode of sizingModes; track mode) {\n <mat-option [value]=\"mode\">\n {{ t('praxis.charts.editor.sizing.' + mode, mode) }}\n </mat-option>\n }\n </mat-select>\n <mat-hint>{{ t('praxis.charts.editor.hint.sizingMode', 'Use fill-container only when the host widget provides a defined body height.') }}</mat-hint>\n </mat-form-field>\n\n @if (sizingModeValue() === 'fixed') {\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.height', 'Height') }}</mat-label>\n <input matInput [ngModel]=\"heightValue()\" (ngModelChange)=\"setSizingHeight($event)\" [disabled]=\"isReadonly()\" />\n <mat-hint>{{ t('praxis.charts.editor.hint.height', 'Numbers are saved as pixels; CSS lengths such as 20rem are also accepted.') }}</mat-hint>\n </mat-form-field>\n }\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.minHeight', 'Minimum height') }}</mat-label>\n <input matInput [ngModel]=\"sizingMinHeightValue()\" (ngModelChange)=\"setSizingMinHeight($event)\" [disabled]=\"isReadonly()\" />\n <mat-hint>{{ t('praxis.charts.editor.hint.minHeight', 'Set a readable minimum for compact dashboard widgets.') }}</mat-hint>\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.maxHeight', 'Maximum height') }}</mat-label>\n <input matInput [ngModel]=\"sizingMaxHeightValue()\" (ngModelChange)=\"setSizingMaxHeight($event)\" [disabled]=\"isReadonly()\" />\n <mat-hint>{{ t('praxis.charts.editor.hint.maxHeight', 'Leave empty unless the chart must stop growing inside a flexible layout.') }}</mat-hint>\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.aspectRatio', 'Aspect ratio') }}</mat-label>\n <input matInput [ngModel]=\"sizingAspectRatioValue()\" (ngModelChange)=\"setSizingAspectRatio($event)\" [disabled]=\"isReadonly()\" />\n <mat-hint>{{ t('praxis.charts.editor.hint.aspectRatio', 'Optional. Use values such as 1.777 or 16 / 9.') }}</mat-hint>\n </mat-form-field>\n </div>\n }\n\n @case ('data') {\n <div class=\"editor-stack\">\n <div class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.sourceKind', 'Source') }}</mat-label>\n <mat-select [ngModel]=\"doc().source.kind\" (ngModelChange)=\"setSourceKind($event)\" [disabled]=\"isReadonly()\">\n @for (sourceKind of sourceKinds; track sourceKind) {\n <mat-option [value]=\"sourceKind\">\n {{ t('praxis.charts.editor.sourceKind.' + (sourceKind === 'praxis.stats' ? 'praxisStats' : 'derived'), sourceKind) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n @if (doc().source.kind === 'praxis.stats') {\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.resource', 'Resource') }}</mat-label>\n @if (resourceOptions().length) {\n <mat-select [ngModel]=\"resourceValue()\" (ngModelChange)=\"setResource($event)\" [disabled]=\"isReadonly()\">\n @for (resource of resourceOptions(); track resource.id) {\n <mat-option [value]=\"resource.path\">{{ resource.label }}</mat-option>\n }\n </mat-select>\n } @else {\n <input matInput [ngModel]=\"resourceValue()\" (ngModelChange)=\"setResource($event)\" [disabled]=\"isReadonly()\" />\n }\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.operation', 'Operation') }}</mat-label>\n <mat-select\n [ngModel]=\"doc().source.operation || 'group-by'\"\n (ngModelChange)=\"setOperation($event)\"\n [disabled]=\"isReadonly()\"\n >\n @for (operation of operations; track operation) {\n <mat-option [value]=\"operation\">\n {{ t('praxis.charts.editor.operation.' + (operation === 'group-by' ? 'groupBy' : operation), operation) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n }\n </div>\n\n @if (showTimeseriesControls()) {\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.specialization.timeseriesTitle', 'Timeseries options') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.granularity', 'Granularity') }}</mat-label>\n <mat-select [ngModel]=\"granularityValue()\" (ngModelChange)=\"setGranularity($event)\" [disabled]=\"isReadonly()\">\n @for (granularity of timeGranularities; track granularity) {\n <mat-option [value]=\"granularity\">\n {{ t('praxis.charts.editor.granularity.' + granularity, granularity) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n <mat-slide-toggle\n [ngModel]=\"fillGapsValue()\"\n (ngModelChange)=\"setFillGaps($event)\"\n [disabled]=\"isReadonly()\"\n >\n {{ t('praxis.charts.editor.field.fillGaps', 'Fill missing intervals') }}\n </mat-slide-toggle>\n </mat-card-content>\n </mat-card>\n }\n\n @if (showDistributionControls()) {\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.specialization.distributionTitle', 'Distribution options') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.distributionMode', 'Distribution mode') }}</mat-label>\n <mat-select [ngModel]=\"distributionModeValue()\" (ngModelChange)=\"setDistributionMode($event)\" [disabled]=\"isReadonly()\">\n @for (mode of distributionModes; track mode) {\n <mat-option [value]=\"mode\">\n {{ t('praxis.charts.editor.distributionMode.' + mode, mode) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.bucketSize', 'Bucket size') }}</mat-label>\n <input matInput [ngModel]=\"bucketSizeValue()\" (ngModelChange)=\"setBucketSize($event)\" [disabled]=\"isReadonly()\" />\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.bucketCount', 'Bucket count') }}</mat-label>\n <input matInput [ngModel]=\"bucketCountValue()\" (ngModelChange)=\"setBucketCount($event)\" [disabled]=\"isReadonly()\" />\n </mat-form-field>\n </mat-card-content>\n </mat-card>\n }\n </div>\n }\n\n @case ('motion') {\n <div class=\"editor-grid\">\n <mat-slide-toggle\n [ngModel]=\"normalizedDocument().motion?.enabled !== false\"\n (ngModelChange)=\"setMotionEnabled($event)\"\n [disabled]=\"isReadonly()\"\n >\n {{ t('praxis.charts.editor.field.motionEnabled', 'Enable animations') }}\n </mat-slide-toggle>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.motionPreset', 'Motion preset') }}</mat-label>\n <mat-select\n [ngModel]=\"normalizedDocument().motion?.preset || 'standard'\"\n (ngModelChange)=\"setMotionPreset($event)\"\n [disabled]=\"isReadonly() || normalizedDocument().motion?.enabled === false\"\n >\n @for (preset of motionPresets; track preset) {\n <mat-option [value]=\"preset\">\n {{ t('praxis.charts.editor.motionPreset.' + preset, preset) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n </div>\n }\n\n @case ('appearance') {\n <div class=\"editor-stack\">\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.appearance.featuresTitle', 'Display features') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-grid\">\n <mat-slide-toggle\n [ngModel]=\"featureEnabled('legend')\"\n (ngModelChange)=\"setFeatureEnabled('legend', $event)\"\n [disabled]=\"isReadonly()\"\n >\n {{ t('praxis.charts.editor.field.legendEnabled', 'Show legend') }}\n </mat-slide-toggle>\n\n <mat-slide-toggle\n [ngModel]=\"featureEnabled('labels')\"\n (ngModelChange)=\"setFeatureEnabled('labels', $event)\"\n [disabled]=\"isReadonly()\"\n >\n {{ t('praxis.charts.editor.field.labelsEnabled', 'Show labels') }}\n </mat-slide-toggle>\n\n <mat-slide-toggle\n [ngModel]=\"featureEnabled('tooltip')\"\n (ngModelChange)=\"setFeatureEnabled('tooltip', $event)\"\n [disabled]=\"isReadonly()\"\n >\n {{ t('praxis.charts.editor.field.tooltipEnabled', 'Show tooltip') }}\n </mat-slide-toggle>\n </mat-card-content>\n </mat-card>\n\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.appearance.paletteTitle', 'Palette') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-stack\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.themeVariant', 'Theme variant') }}</mat-label>\n <mat-select [ngModel]=\"themeVariantValue()\" (ngModelChange)=\"setThemeVariant($event)\" [disabled]=\"isReadonly()\">\n <mat-option [value]=\"''\">\n {{ t('praxis.charts.editor.themeVariant.none', 'No variant') }}\n </mat-option>\n @for (variant of themeVariants; track variant) {\n <mat-option [value]=\"variant\">\n {{ t('praxis.charts.editor.themeVariant.' + variant, variant) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.paletteMode', 'Palette mode') }}</mat-label>\n <mat-select [ngModel]=\"paletteModeValue()\" (ngModelChange)=\"setPaletteMode($event)\" [disabled]=\"isReadonly()\">\n @for (mode of paletteModes; track mode) {\n <mat-option [value]=\"mode\">\n {{ t('praxis.charts.editor.paletteMode.' + mode, mode) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n @if (paletteModeValue() === 'token') {\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.paletteToken', 'Palette token') }}</mat-label>\n <mat-select [ngModel]=\"paletteTokenValue()\" (ngModelChange)=\"setPaletteToken($event)\" [disabled]=\"isReadonly()\">\n @for (token of paletteTokens; track token) {\n <mat-option [value]=\"token\">\n {{ t('praxis.charts.editor.paletteToken.' + token, token) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n } @else {\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.palette', 'Palette colors') }}</mat-label>\n <textarea\n matInput\n rows=\"3\"\n [ngModel]=\"paletteValue()\"\n (ngModelChange)=\"setPalette($event)\"\n [disabled]=\"isReadonly()\"\n ></textarea>\n </mat-form-field>\n }\n\n <p class=\"editor-caption\">\n {{ t('praxis.charts.editor.appearance.paletteHint', 'Use a registered token or comma-separated colors to persist theme.palette in the canonical contract.') }}\n </p>\n </mat-card-content>\n </mat-card>\n\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.appearance.surfaceTitle', 'Surface') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content>\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.surfaceMode', 'Surface mode') }}</mat-label>\n <mat-select [ngModel]=\"surfaceModeValue()\" (ngModelChange)=\"setSurfaceMode($event)\" [disabled]=\"isReadonly()\">\n @for (mode of surfaceModes; track mode) {\n <mat-option [value]=\"mode\">\n {{ t('praxis.charts.editor.surface.' + mode, mode) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n <p class=\"editor-caption\">\n {{ t('praxis.charts.editor.appearance.surfaceHint', 'Use embedded for dashboard widgets and contained only when the chart must own its visual surface.') }}\n </p>\n </mat-card-content>\n </mat-card>\n\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.appearance.statesTitle', 'State messages') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-stack\">\n <div class=\"editor-row-card\">\n <div class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.emptyTitle', 'Empty title') }}</mat-label>\n <input matInput [ngModel]=\"stateTitle('empty')\" (ngModelChange)=\"setStateTitle('empty', $event)\" [disabled]=\"isReadonly()\" />\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.emptyDescription', 'Empty description') }}</mat-label>\n <textarea matInput rows=\"2\" [ngModel]=\"stateDescription('empty')\" (ngModelChange)=\"setStateDescription('empty', $event)\" [disabled]=\"isReadonly()\"></textarea>\n </mat-form-field>\n </div>\n </div>\n\n <div class=\"editor-row-card\">\n <div class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.loadingTitle', 'Loading title') }}</mat-label>\n <input matInput [ngModel]=\"stateTitle('loading')\" (ngModelChange)=\"setStateTitle('loading', $event)\" [disabled]=\"isReadonly()\" />\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.loadingDescription', 'Loading description') }}</mat-label>\n <textarea matInput rows=\"2\" [ngModel]=\"stateDescription('loading')\" (ngModelChange)=\"setStateDescription('loading', $event)\" [disabled]=\"isReadonly()\"></textarea>\n </mat-form-field>\n </div>\n </div>\n\n <div class=\"editor-row-card\">\n <div class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.errorTitle', 'Error title') }}</mat-label>\n <input matInput [ngModel]=\"stateTitle('error')\" (ngModelChange)=\"setStateTitle('error', $event)\" [disabled]=\"isReadonly()\" />\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.errorDescription', 'Error description') }}</mat-label>\n <textarea matInput rows=\"2\" [ngModel]=\"stateDescription('error')\" (ngModelChange)=\"setStateDescription('error', $event)\" [disabled]=\"isReadonly()\"></textarea>\n </mat-form-field>\n </div>\n </div>\n </mat-card-content>\n </mat-card>\n </div>\n }\n\n @case ('analytics') {\n <div class=\"editor-stack\">\n @if (showComboPanel()) {\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.specialization.comboTitle', 'Combo guidance') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content>\n <p class=\"editor-caption\">\n {{ t('praxis.charts.editor.specialization.comboHint', 'Combo charts require at least two metrics and allow per-metric axis and series kind mapping.') }}\n </p>\n </mat-card-content>\n </mat-card>\n }\n\n @if (showPieDonutPanel()) {\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.specialization.pieDonutTitle', 'Composition guidance') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content>\n <p class=\"editor-caption\">\n {{ t('praxis.charts.editor.specialization.pieDonutHint', 'Pie and donut charts keep only the first metric and use the first dimension as the category segment.') }}\n </p>\n </mat-card-content>\n </mat-card>\n }\n\n @if (showScatterPanel()) {\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.specialization.scatterTitle', 'Scatter guidance') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content>\n <p class=\"editor-caption\">\n {{ t('praxis.charts.editor.specialization.scatterHint', 'Scatter charts use the first dimension as X and the first metric as Y, so keep both fields mapped.') }}\n </p>\n </mat-card-content>\n </mat-card>\n }\n\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.analytics.dimensionsTitle', 'Dimensions') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-stack\">\n @for (dimension of dimensions(); track $index) {\n <div class=\"editor-row-card\" [attr.data-testid]=\"'chart-editor-dimension-row-' + $index\">\n <div class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\" [attr.data-testid]=\"'chart-editor-dimension-field-' + $index\">\n <mat-label>{{ t('praxis.charts.editor.field.dimension', 'Dimension') }}</mat-label>\n @if (fieldOptions('dimension').length) {\n <mat-select [ngModel]=\"dimension.field || ''\" (ngModelChange)=\"setDimensionField($index, $event)\" [disabled]=\"isReadonly()\">\n @for (field of fieldOptions('dimension'); track field.field) {\n <mat-option [value]=\"field.field\">{{ field.label || field.field }}</mat-option>\n }\n </mat-select>\n } @else {\n <input matInput [ngModel]=\"dimension.field || ''\" (ngModelChange)=\"setDimensionField($index, $event)\" [disabled]=\"isReadonly()\" />\n }\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\" [attr.data-testid]=\"'chart-editor-dimension-role-' + $index\">\n <mat-label>{{ t('praxis.charts.editor.field.dimensionRole', 'Dimension role') }}</mat-label>\n <mat-select [ngModel]=\"dimension.role || 'category'\" (ngModelChange)=\"setDimensionRole($index, $event)\" [disabled]=\"isReadonly()\">\n @for (role of dimensionRoles; track role) {\n <mat-option [value]=\"role\">\n {{ t('praxis.charts.editor.dimensionRole.' + role, role) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n </div>\n\n <div class=\"editor-row-actions\">\n <button mat-button type=\"button\" (click)=\"removeDimension($index)\" [disabled]=\"isReadonly() || dimensions().length <= 1\">\n {{ t('praxis.charts.editor.analytics.removeDimension', 'Remove dimension') }}\n </button>\n </div>\n </div>\n }\n\n <div>\n <button mat-stroked-button type=\"button\" data-testid=\"chart-editor-add-dimension\" (click)=\"addDimension()\" [disabled]=\"isReadonly()\">\n {{ t('praxis.charts.editor.analytics.addDimension', 'Add dimension') }}\n </button>\n </div>\n </mat-card-content>\n </mat-card>\n\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.analytics.metricsTitle', 'Metrics') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-stack\">\n @for (metric of metrics(); track $index) {\n <div class=\"editor-row-card\" [attr.data-testid]=\"'chart-editor-metric-row-' + $index\">\n <div class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\" [attr.data-testid]=\"'chart-editor-metric-field-' + $index\">\n <mat-label>{{ t('praxis.charts.editor.field.metric', 'Metric') }}</mat-label>\n @if (fieldOptions('metric').length) {\n <mat-select [ngModel]=\"metric.field || ''\" (ngModelChange)=\"setMetricField($index, $event)\" [disabled]=\"isReadonly()\">\n @for (field of fieldOptions('metric'); track field.field) {\n <mat-option [value]=\"field.field\">{{ field.label || field.field }}</mat-option>\n }\n </mat-select>\n } @else {\n <input matInput [ngModel]=\"metric.field || ''\" (ngModelChange)=\"setMetricField($index, $event)\" [disabled]=\"isReadonly()\" />\n }\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\" [attr.data-testid]=\"'chart-editor-metric-label-' + $index\">\n <mat-label>{{ t('praxis.charts.editor.field.metricLabel', 'Metric label') }}</mat-label>\n <input matInput [ngModel]=\"metric.label || ''\" (ngModelChange)=\"setMetricLabel($index, $event)\" [disabled]=\"isReadonly()\" />\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\" [attr.data-testid]=\"'chart-editor-metric-aggregation-' + $index\">\n <mat-label>{{ t('praxis.charts.editor.field.metricAggregation', 'Aggregation') }}</mat-label>\n <mat-select [ngModel]=\"metric.aggregation || 'sum'\" (ngModelChange)=\"setMetricAggregation($index, $event)\" [disabled]=\"isReadonly()\">\n @for (aggregation of metricAggregations; track aggregation) {\n <mat-option [value]=\"aggregation\">\n {{ t('praxis.charts.editor.metricAggregation.' + aggregation, aggregation) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n @if (showMetricAxisControls()) {\n <mat-form-field class=\"editor-field\" appearance=\"outline\" [attr.data-testid]=\"'chart-editor-metric-axis-' + $index\">\n <mat-label>{{ t('praxis.charts.editor.field.metricAxis', 'Axis') }}</mat-label>\n <mat-select [ngModel]=\"metric.axis || 'primary'\" (ngModelChange)=\"setMetricAxis($index, $event)\" [disabled]=\"isReadonly()\">\n @for (axis of metricAxes; track axis) {\n <mat-option [value]=\"axis\">\n {{ t('praxis.charts.editor.metricAxis.' + axis, axis) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n }\n\n @if (showMetricSeriesKindControls()) {\n <mat-form-field class=\"editor-field\" appearance=\"outline\" [attr.data-testid]=\"'chart-editor-metric-series-kind-' + $index\">\n <mat-label>{{ t('praxis.charts.editor.field.metricSeriesKind', 'Series kind') }}</mat-label>\n <mat-select [ngModel]=\"metric.seriesKind || 'bar'\" (ngModelChange)=\"setMetricSeriesKind($index, $event)\" [disabled]=\"isReadonly()\">\n @for (seriesKind of metricSeriesKinds; track seriesKind) {\n <mat-option [value]=\"seriesKind\">\n {{ t('praxis.charts.editor.metricSeriesKind.' + seriesKind, seriesKind) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n }\n </div>\n\n <div class=\"editor-row-actions\">\n <button mat-button type=\"button\" [attr.data-testid]=\"'chart-editor-remove-metric-' + $index\" (click)=\"removeMetric($index)\" [disabled]=\"isReadonly() || metrics().length <= 1\">\n {{ t('praxis.charts.editor.analytics.removeMetric', 'Remove metric') }}\n </button>\n </div>\n </div>\n }\n\n <div>\n <button mat-stroked-button type=\"button\" data-testid=\"chart-editor-add-metric\" (click)=\"addMetric()\" [disabled]=\"isReadonly()\">\n {{ t('praxis.charts.editor.analytics.addMetric', 'Add metric') }}\n </button>\n </div>\n </mat-card-content>\n </mat-card>\n </div>\n }\n\n @case ('events') {\n <div class=\"editor-stack\">\n <mat-card class=\"editor-card\" data-testid=\"chart-editor-event-pointClick\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.events.pointClickTitle', 'Point click') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventAction', 'Action') }}</mat-label>\n <mat-select data-testid=\"chart-editor-event-pointClick-action\" [ngModel]=\"eventAction('pointClick')\" (ngModelChange)=\"setEventAction('pointClick', $event)\" [disabled]=\"isReadonly()\">\n <mat-option value=\"\">{{ t('praxis.charts.editor.events.none', 'None') }}</mat-option>\n @for (action of eventActionOptions; track action) {\n <mat-option [value]=\"action\">\n {{ t('praxis.charts.editor.eventAction.' + action, action) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventTarget', 'Target') }}</mat-label>\n @if (targetOptions().length) {\n <mat-select data-testid=\"chart-editor-event-pointClick-target\" [ngModel]=\"eventTarget('pointClick')\" (ngModelChange)=\"setEventTarget('pointClick', $event)\" [disabled]=\"isReadonly() || !eventAction('pointClick')\">\n @for (target of targetOptions(); track target.id) {\n <mat-option [value]=\"target.id\">{{ target.label }}</mat-option>\n }\n </mat-select>\n } @else {\n <input matInput data-testid=\"chart-editor-event-pointClick-target\" [ngModel]=\"eventTarget('pointClick')\" (ngModelChange)=\"setEventTarget('pointClick', $event)\" [disabled]=\"isReadonly() || !eventAction('pointClick')\" />\n }\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventMapping', 'Mapping') }}</mat-label>\n <textarea\n matInput\n data-testid=\"chart-editor-event-pointClick-mapping\"\n rows=\"4\"\n [ngModel]=\"eventMappingText('pointClick')\"\n (ngModelChange)=\"setEventMapping('pointClick', $event)\"\n [disabled]=\"isReadonly() || !eventAction('pointClick')\"\n ></textarea>\n </mat-form-field>\n </mat-card-content>\n </mat-card>\n\n <mat-card class=\"editor-card\" data-testid=\"chart-editor-event-selectionChange\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.events.selectionChangeTitle', 'Selection change') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventAction', 'Action') }}</mat-label>\n <mat-select data-testid=\"chart-editor-event-selectionChange-action\" [ngModel]=\"eventAction('selectionChange')\" (ngModelChange)=\"setEventAction('selectionChange', $event)\" [disabled]=\"isReadonly()\">\n <mat-option value=\"\">{{ t('praxis.charts.editor.events.none', 'None') }}</mat-option>\n @for (action of eventActionOptions; track action) {\n <mat-option [value]=\"action\">\n {{ t('praxis.charts.editor.eventAction.' + action, action) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventTarget', 'Target') }}</mat-label>\n @if (targetOptions().length) {\n <mat-select data-testid=\"chart-editor-event-selectionChange-target\" [ngModel]=\"eventTarget('selectionChange')\" (ngModelChange)=\"setEventTarget('selectionChange', $event)\" [disabled]=\"isReadonly() || !eventAction('selectionChange')\">\n @for (target of targetOptions(); track target.id) {\n <mat-option [value]=\"target.id\">{{ target.label }}</mat-option>\n }\n </mat-select>\n } @else {\n <input matInput data-testid=\"chart-editor-event-selectionChange-target\" [ngModel]=\"eventTarget('selectionChange')\" (ngModelChange)=\"setEventTarget('selectionChange', $event)\" [disabled]=\"isReadonly() || !eventAction('selectionChange')\" />\n }\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventMapping', 'Mapping') }}</mat-label>\n <textarea\n matInput\n data-testid=\"chart-editor-event-selectionChange-mapping\"\n rows=\"4\"\n [ngModel]=\"eventMappingText('selectionChange')\"\n (ngModelChange)=\"setEventMapping('selectionChange', $event)\"\n [disabled]=\"isReadonly() || !eventAction('selectionChange')\"\n ></textarea>\n </mat-form-field>\n </mat-card-content>\n </mat-card>\n\n <mat-card class=\"editor-card\" data-testid=\"chart-editor-event-drillDown\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.events.drillDownTitle', 'Drill down') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventAction', 'Action') }}</mat-label>\n <mat-select data-testid=\"chart-editor-event-drillDown-action\" [ngModel]=\"eventAction('drillDown')\" (ngModelChange)=\"setEventAction('drillDown', $event)\" [disabled]=\"isReadonly()\">\n <mat-option value=\"\">{{ t('praxis.charts.editor.events.none', 'None') }}</mat-option>\n @for (action of eventActionOptions; track action) {\n <mat-option [value]=\"action\">\n {{ t('praxis.charts.editor.eventAction.' + action, action) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventTarget', 'Target') }}</mat-label>\n @if (targetOptions().length) {\n <mat-select data-testid=\"chart-editor-event-drillDown-target\" [ngModel]=\"eventTarget('drillDown')\" (ngModelChange)=\"setEventTarget('drillDown', $event)\" [disabled]=\"isReadonly() || !eventAction('drillDown')\">\n @for (target of targetOptions(); track target.id) {\n <mat-option [value]=\"target.id\">{{ target.label }}</mat-option>\n }\n </mat-select>\n } @else {\n <input matInput data-testid=\"chart-editor-event-drillDown-target\" [ngModel]=\"eventTarget('drillDown')\" (ngModelChange)=\"setEventTarget('drillDown', $event)\" [disabled]=\"isReadonly() || !eventAction('drillDown')\" />\n }\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventMapping', 'Mapping') }}</mat-label>\n <textarea\n matInput\n data-testid=\"chart-editor-event-drillDown-mapping\"\n rows=\"4\"\n [ngModel]=\"eventMappingText('drillDown')\"\n (ngModelChange)=\"setEventMapping('drillDown', $event)\"\n [disabled]=\"isReadonly() || !eventAction('drillDown')\"\n ></textarea>\n </mat-form-field>\n </mat-card-content>\n </mat-card>\n\n <mat-card class=\"editor-card\" data-testid=\"chart-editor-event-crossFilter\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.events.crossFilterTitle', 'Cross filter') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventAction', 'Action') }}</mat-label>\n <mat-select data-testid=\"chart-editor-event-crossFilter-action\" [ngModel]=\"eventAction('crossFilter')\" (ngModelChange)=\"setEventAction('crossFilter', $event)\" [disabled]=\"isReadonly()\">\n <mat-option value=\"\">{{ t('praxis.charts.editor.events.none', 'None') }}</mat-option>\n @for (action of eventActionOptions; track action) {\n <mat-option [value]=\"action\">\n {{ t('praxis.charts.editor.eventAction.' + action, action) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventTarget', 'Target') }}</mat-label>\n @if (targetOptions().length) {\n <mat-select data-testid=\"chart-editor-event-crossFilter-target\" [ngModel]=\"eventTarget('crossFilter')\" (ngModelChange)=\"setEventTarget('crossFilter', $event)\" [disabled]=\"isReadonly() || !eventAction('crossFilter')\">\n @for (target of targetOptions(); track target.id) {\n <mat-option [value]=\"target.id\">{{ target.label }}</mat-option>\n }\n </mat-select>\n } @else {\n <input matInput data-testid=\"chart-editor-event-crossFilter-target\" [ngModel]=\"eventTarget('crossFilter')\" (ngModelChange)=\"setEventTarget('crossFilter', $event)\" [disabled]=\"isReadonly() || !eventAction('crossFilter')\" />\n }\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventMapping', 'Mapping') }}</mat-label>\n <textarea\n matInput\n data-testid=\"chart-editor-event-crossFilter-mapping\"\n rows=\"4\"\n [ngModel]=\"eventMappingText('crossFilter')\"\n (ngModelChange)=\"setEventMapping('crossFilter', $event)\"\n [disabled]=\"isReadonly() || !eventAction('crossFilter')\"\n ></textarea>\n </mat-form-field>\n </mat-card-content>\n </mat-card>\n </div>\n }\n\n @case ('preview') {\n <p class=\"editor-caption\">\n {{ t('praxis.charts.editor.preview.caption', 'Local preview derived from the canonical contract without remote calls.') }}\n </p>\n }\n }\n </mat-card-content>\n </mat-card>\n </div>\n\n <div class=\"editor-side\">\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.issues.title', 'Validation issues') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content>\n @if (issues().length) {\n <ul class=\"editor-issues\">\n @for (issue of issues(); track issueTrackBy($index, issue)) {\n <li class=\"editor-issue\">\n <strong>{{ issue.field }}</strong>\n <span>{{ issue.message }}</span>\n </li>\n }\n </ul>\n } @else {\n <div class=\"editor-empty\">\n {{ t('praxis.charts.editor.issues.empty', 'No issues were identified.') }}\n </div>\n }\n </mat-card-content>\n </mat-card>\n\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.preview.title', 'Chart preview') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content>\n @if (preview(); as chartPreview) {\n <p class=\"editor-caption\">\n {{ t('praxis.charts.editor.preview.caption', 'Local preview derived from the canonical contract without remote calls.') }}\n </p>\n <praxis-chart [config]=\"chartPreview.config\" [data]=\"chartPreview.data\"></praxis-chart>\n } @else {\n <div class=\"editor-empty\">\n {{ t('praxis.charts.editor.preview.invalid', 'Preview is unavailable while the contract has blocking errors.') }}\n </div>\n }\n </mat-card-content>\n </mat-card>\n </div>\n </div>\n</div>\n", styles: [":host{display:block;min-width:0;color:var(--md-sys-color-on-surface, #1a1b20)}.editor-shell{display:grid;gap:18px}.editor-nav{display:flex;gap:8px;flex-wrap:wrap}.editor-nav button.active{background:color-mix(in srgb,var(--md-sys-color-primary, #1263b4) 18%,transparent);color:var(--md-sys-color-primary, #1263b4)}.editor-layout{display:grid;gap:18px;grid-template-columns:minmax(0,1.35fr) minmax(320px,.9fr);align-items:start}.editor-form,.editor-side{display:grid;gap:16px}.editor-card{border-radius:20px;border:1px solid color-mix(in srgb,var(--md-sys-color-outline, #c5c7ce) 72%,transparent);background:linear-gradient(180deg,#1263b408,#1263b400)}.editor-grid{display:grid;gap:14px;grid-template-columns:repeat(auto-fit,minmax(220px,1fr))}.editor-stack{display:grid;gap:14px}.editor-row-card{padding:14px;border-radius:16px;border:1px solid color-mix(in srgb,var(--md-sys-color-outline, #c5c7ce) 54%,transparent);background:color-mix(in srgb,var(--md-sys-color-surface, #fff) 92%,rgba(18,99,180,.04))}.editor-row-actions{display:flex;justify-content:flex-end}.editor-field{width:100%}.editor-issues{display:grid;gap:10px;margin:0;padding:0;list-style:none}.editor-issue{padding:12px 14px;border-radius:14px;background:color-mix(in srgb,var(--md-sys-color-error, #b3261e) 8%,transparent);border:1px solid color-mix(in srgb,var(--md-sys-color-error, #b3261e) 18%,transparent)}.editor-issue strong{display:block;margin-bottom:4px}.editor-caption{margin:0 0 12px;color:var(--md-sys-color-on-surface-variant, #5a5d67);font-size:.92rem}.editor-empty{padding:18px;border-radius:14px;background:color-mix(in srgb,var(--md-sys-color-surface-variant, #eceff4) 78%,transparent)}@media(max-width:960px){.editor-layout{grid-template-columns:minmax(0,1fr)}}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$3.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$3.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$3.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i1$2.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: MatCardModule }, { kind: "component", type: i3$1.MatCard, selector: "mat-card", inputs: ["appearance"], exportAs: ["matCard"] }, { kind: "directive", type: i3$1.MatCardContent, selector: "mat-card-content" }, { kind: "component", type: i3$1.MatCardHeader, selector: "mat-card-header" }, { kind: "directive", type: i3$1.MatCardTitle, selector: "mat-card-title, [mat-card-title], [matCardTitle]" }, { kind: "ngmodule", type: MatFormFieldModule }, { kind: "component", type: i4.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i4.MatLabel, selector: "mat-label" }, { kind: "directive", type: i4.MatHint, selector: "mat-hint", inputs: ["align", "id"] }, { kind: "ngmodule", type: MatInputModule }, { kind: "directive", type: i5.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["disabled", "id", "placeholder", "name", "required", "type", "errorStateMatcher", "aria-describedby", "value", "readonly", "disabledInteractive"], exportAs: ["matInput"] }, { kind: "ngmodule", type: MatSelectModule }, { kind: "component", type: i6.MatSelect, selector: "mat-select", inputs: ["aria-describedby", "panelClass", "disabled", "disableRipple", "tabIndex", "hideSingleSelectionIndicator", "placeholder", "required", "multiple", "disableOptionCentering", "compareWith", "value", "aria-label", "aria-labelledby", "errorStateMatcher", "typeaheadDebounceInterval", "sortComparator", "id", "panelWidth", "canSelectNullableOptions"], outputs: ["openedChange", "opened", "closed", "selectionChange", "valueChange"], exportAs: ["matSelect"] }, { kind: "component", type: i6.MatOption, selector: "mat-option", inputs: ["value", "id", "disabled"], outputs: ["onSelectionChange"], exportAs: ["matOption"] }, { kind: "ngmodule", type: MatSlideToggleModule }, { kind: "component", type: i7.MatSlideToggle, selector: "mat-slide-toggle", inputs: ["name", "id", "labelPosition", "aria-label", "aria-labelledby", "aria-describedby", "required", "color", "disabled", "disableRipple", "tabIndex", "checked", "hideIcon", "disabledInteractive"], outputs: ["change", "toggleChange"], exportAs: ["matSlideToggle"] }, { kind: "component", type: PraxisChartComponent, selector: "praxis-chart", inputs: ["config", "data", "chartDocument", "filterCriteria", "queryContext", "remoteDataResolver", "enableCustomization", "availableResources", "availableFields", "availableTargets"], outputs: ["pointClick", "selectionChange", "crossFilter", "queryRequest", "loadStateChange", "chartDocumentApplied", "chartDocumentSaved"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
6839
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.14", type: PraxisChartConfigEditor, isStandalone: true, selector: "praxis-chart-config-editor", inputs: { documentInput: { classPropertyName: "documentInput", publicName: "document", isSignal: true, isRequired: false, transformFunction: null }, modeInput: { classPropertyName: "modeInput", publicName: "mode", isSignal: true, isRequired: false, transformFunction: null }, readonlyInput: { classPropertyName: "readonlyInput", publicName: "readonly", isSignal: true, isRequired: false, transformFunction: null }, availableResourcesInput: { classPropertyName: "availableResourcesInput", publicName: "availableResources", isSignal: true, isRequired: false, transformFunction: null }, availableFieldsInput: { classPropertyName: "availableFieldsInput", publicName: "availableFields", isSignal: true, isRequired: false, transformFunction: null }, availableTargetsInput: { classPropertyName: "availableTargetsInput", publicName: "availableTargets", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { apply: "apply", save: "save", resetChange: "resetChange", documentChange: "documentChange" }, providers: [providePraxisChartsI18n()], ngImport: i0, template: "<div class=\"editor-shell\">\n <div class=\"editor-nav\">\n @for (section of sections; track section.id) {\n <button\n mat-stroked-button\n type=\"button\"\n [attr.data-testid]=\"'chart-editor-section-' + section.id\"\n [class.active]=\"activeSection() === section.id\"\n (click)=\"setSection(section.id)\"\n >\n {{ t(section.labelKey, section.fallback) }}\n </button>\n }\n </div>\n\n <div class=\"editor-layout\">\n <div class=\"editor-form\">\n <mat-card class=\"editor-card\">\n <mat-card-content>\n @switch (activeSection()) {\n @case ('general') {\n <div class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.chartId', 'Chart ID') }}</mat-label>\n <input matInput [ngModel]=\"doc().chartId || ''\" (ngModelChange)=\"setChartId($event)\" [disabled]=\"isReadonly()\" />\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.kind', 'Kind') }}</mat-label>\n <mat-select [ngModel]=\"doc().kind\" (ngModelChange)=\"setKind($event)\" [disabled]=\"isReadonly()\">\n @for (kind of chartKinds; track kind) {\n <mat-option [value]=\"kind\">\n {{ t('praxis.charts.editor.kind.' + kind, kind) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.title', 'Title') }}</mat-label>\n <input matInput data-testid=\"chart-editor-title-input\" [ngModel]=\"titleValue()\" (ngModelChange)=\"setTitle($event)\" [disabled]=\"isReadonly()\" />\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.subtitle', 'Subtitle') }}</mat-label>\n <input matInput [ngModel]=\"subtitleValue()\" (ngModelChange)=\"setSubtitle($event)\" [disabled]=\"isReadonly()\" />\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.sizingMode', 'Sizing') }}</mat-label>\n <mat-select [ngModel]=\"sizingModeValue()\" (ngModelChange)=\"setSizingMode($event)\" [disabled]=\"isReadonly()\">\n @for (mode of sizingModes; track mode) {\n <mat-option [value]=\"mode\">\n {{ t('praxis.charts.editor.sizing.' + mode, mode) }}\n </mat-option>\n }\n </mat-select>\n <mat-hint>{{ t('praxis.charts.editor.hint.sizingMode', 'Use fill-container only when the host widget provides a defined body height.') }}</mat-hint>\n </mat-form-field>\n\n @if (sizingModeValue() === 'fixed') {\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.height', 'Height') }}</mat-label>\n <input matInput [ngModel]=\"heightValue()\" (ngModelChange)=\"setSizingHeight($event)\" [disabled]=\"isReadonly()\" />\n <mat-hint>{{ t('praxis.charts.editor.hint.height', 'Numbers are saved as pixels; CSS lengths such as 20rem are also accepted.') }}</mat-hint>\n </mat-form-field>\n }\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.minHeight', 'Minimum height') }}</mat-label>\n <input matInput [ngModel]=\"sizingMinHeightValue()\" (ngModelChange)=\"setSizingMinHeight($event)\" [disabled]=\"isReadonly()\" />\n <mat-hint>{{ t('praxis.charts.editor.hint.minHeight', 'Set a readable minimum for compact dashboard widgets.') }}</mat-hint>\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.maxHeight', 'Maximum height') }}</mat-label>\n <input matInput [ngModel]=\"sizingMaxHeightValue()\" (ngModelChange)=\"setSizingMaxHeight($event)\" [disabled]=\"isReadonly()\" />\n <mat-hint>{{ t('praxis.charts.editor.hint.maxHeight', 'Leave empty unless the chart must stop growing inside a flexible layout.') }}</mat-hint>\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.aspectRatio', 'Aspect ratio') }}</mat-label>\n <input matInput [ngModel]=\"sizingAspectRatioValue()\" (ngModelChange)=\"setSizingAspectRatio($event)\" [disabled]=\"isReadonly()\" />\n <mat-hint>{{ t('praxis.charts.editor.hint.aspectRatio', 'Optional. Use values such as 1.777 or 16 / 9.') }}</mat-hint>\n </mat-form-field>\n </div>\n }\n\n @case ('data') {\n <div class=\"editor-stack\">\n <div class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.sourceKind', 'Source') }}</mat-label>\n <mat-select data-testid=\"chart-editor-source-kind\" [ngModel]=\"doc().source.kind\" (ngModelChange)=\"setSourceKind($event)\" [disabled]=\"isReadonly()\">\n @for (sourceKind of sourceKinds; track sourceKind) {\n <mat-option [value]=\"sourceKind\">\n {{ t('praxis.charts.editor.sourceKind.' + (sourceKind === 'praxis.stats' ? 'praxisStats' : 'derived'), sourceKind) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n @if (doc().source.kind === 'praxis.stats') {\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.resource', 'Resource') }}</mat-label>\n <mat-select data-testid=\"chart-editor-resource\" [ngModel]=\"resourceValue()\" (ngModelChange)=\"setResource($event)\" [disabled]=\"isReadonly() || resourceCatalogUnavailable()\">\n @for (resource of resourceOptions(); track resource.id) {\n <mat-option [value]=\"resource.path\">{{ resource.label }}</mat-option>\n }\n </mat-select>\n @if (resourceCatalogUnavailable()) {\n <mat-hint>{{ t('praxis.charts.editor.catalog.resourceMissing', 'Resource catalog is required for governed praxis.stats authoring.') }}</mat-hint>\n }\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.operation', 'Operation') }}</mat-label>\n <mat-select\n data-testid=\"chart-editor-operation\"\n [ngModel]=\"doc().source.operation || 'group-by'\"\n (ngModelChange)=\"setOperation($event)\"\n [disabled]=\"isReadonly() || operationCatalogUnavailable()\"\n >\n @for (operation of operationOptions(); track operation) {\n <mat-option [value]=\"operation\">\n {{ t('praxis.charts.editor.operation.' + (operation === 'group-by' ? 'groupBy' : operation), operation) }}\n </mat-option>\n }\n </mat-select>\n @if (operationCatalogUnavailable()) {\n <mat-hint>{{ t('praxis.charts.editor.catalog.operationMissing', 'Selected resource does not publish authorable stats operations.') }}</mat-hint>\n }\n </mat-form-field>\n }\n </div>\n\n @if (showTimeseriesControls()) {\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.specialization.timeseriesTitle', 'Timeseries options') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.granularity', 'Granularity') }}</mat-label>\n <mat-select [ngModel]=\"granularityValue()\" (ngModelChange)=\"setGranularity($event)\" [disabled]=\"isReadonly()\">\n @for (granularity of timeGranularities; track granularity) {\n <mat-option [value]=\"granularity\">\n {{ t('praxis.charts.editor.granularity.' + granularity, granularity) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n <mat-slide-toggle\n [ngModel]=\"fillGapsValue()\"\n (ngModelChange)=\"setFillGaps($event)\"\n [disabled]=\"isReadonly()\"\n >\n {{ t('praxis.charts.editor.field.fillGaps', 'Fill missing intervals') }}\n </mat-slide-toggle>\n </mat-card-content>\n </mat-card>\n }\n\n @if (showDistributionControls()) {\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.specialization.distributionTitle', 'Distribution options') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.distributionMode', 'Distribution mode') }}</mat-label>\n <mat-select data-testid=\"chart-editor-distribution-mode\" [ngModel]=\"distributionModeValue()\" (ngModelChange)=\"setDistributionMode($event)\" [disabled]=\"isReadonly()\">\n @for (mode of distributionModes; track mode) {\n <mat-option [value]=\"mode\">\n {{ t('praxis.charts.editor.distributionMode.' + mode, mode) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n @if (distributionModeValue() === 'histogram') {\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.bucketSize', 'Bucket size') }}</mat-label>\n <input matInput data-testid=\"chart-editor-bucket-size\" [ngModel]=\"bucketSizeValue()\" (ngModelChange)=\"setBucketSize($event)\" [disabled]=\"isReadonly()\" />\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.bucketCount', 'Bucket count') }}</mat-label>\n <input matInput data-testid=\"chart-editor-bucket-count\" [ngModel]=\"bucketCountValue()\" (ngModelChange)=\"setBucketCount($event)\" [disabled]=\"isReadonly()\" />\n </mat-form-field>\n }\n </mat-card-content>\n </mat-card>\n }\n </div>\n }\n\n @case ('motion') {\n <div class=\"editor-grid\">\n <mat-slide-toggle\n [ngModel]=\"normalizedDocument().motion?.enabled !== false\"\n (ngModelChange)=\"setMotionEnabled($event)\"\n [disabled]=\"isReadonly()\"\n >\n {{ t('praxis.charts.editor.field.motionEnabled', 'Enable animations') }}\n </mat-slide-toggle>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.motionPreset', 'Motion preset') }}</mat-label>\n <mat-select\n [ngModel]=\"normalizedDocument().motion?.preset || 'standard'\"\n (ngModelChange)=\"setMotionPreset($event)\"\n [disabled]=\"isReadonly() || normalizedDocument().motion?.enabled === false\"\n >\n @for (preset of motionPresets; track preset) {\n <mat-option [value]=\"preset\">\n {{ t('praxis.charts.editor.motionPreset.' + preset, preset) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n </div>\n }\n\n @case ('appearance') {\n <div class=\"editor-stack\">\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.appearance.featuresTitle', 'Display features') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-grid\">\n <mat-slide-toggle\n [ngModel]=\"featureEnabled('legend')\"\n (ngModelChange)=\"setFeatureEnabled('legend', $event)\"\n [disabled]=\"isReadonly()\"\n >\n {{ t('praxis.charts.editor.field.legendEnabled', 'Show legend') }}\n </mat-slide-toggle>\n\n <mat-slide-toggle\n [ngModel]=\"featureEnabled('labels')\"\n (ngModelChange)=\"setFeatureEnabled('labels', $event)\"\n [disabled]=\"isReadonly()\"\n >\n {{ t('praxis.charts.editor.field.labelsEnabled', 'Show labels') }}\n </mat-slide-toggle>\n\n <mat-slide-toggle\n [ngModel]=\"featureEnabled('tooltip')\"\n (ngModelChange)=\"setFeatureEnabled('tooltip', $event)\"\n [disabled]=\"isReadonly()\"\n >\n {{ t('praxis.charts.editor.field.tooltipEnabled', 'Show tooltip') }}\n </mat-slide-toggle>\n </mat-card-content>\n </mat-card>\n\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.appearance.paletteTitle', 'Palette') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-stack\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.themeVariant', 'Theme variant') }}</mat-label>\n <mat-select [ngModel]=\"themeVariantValue()\" (ngModelChange)=\"setThemeVariant($event)\" [disabled]=\"isReadonly()\">\n <mat-option [value]=\"''\">\n {{ t('praxis.charts.editor.themeVariant.none', 'No variant') }}\n </mat-option>\n @for (variant of themeVariants; track variant) {\n <mat-option [value]=\"variant\">\n {{ t('praxis.charts.editor.themeVariant.' + variant, variant) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.paletteMode', 'Palette mode') }}</mat-label>\n <mat-select [ngModel]=\"paletteModeValue()\" (ngModelChange)=\"setPaletteMode($event)\" [disabled]=\"isReadonly()\">\n @for (mode of paletteModes; track mode) {\n <mat-option [value]=\"mode\">\n {{ t('praxis.charts.editor.paletteMode.' + mode, mode) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n @if (paletteModeValue() === 'token') {\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.paletteToken', 'Palette token') }}</mat-label>\n <mat-select [ngModel]=\"paletteTokenValue()\" (ngModelChange)=\"setPaletteToken($event)\" [disabled]=\"isReadonly()\">\n @for (token of paletteTokens; track token) {\n <mat-option [value]=\"token\">\n {{ t('praxis.charts.editor.paletteToken.' + token, token) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n } @else {\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.palette', 'Palette colors') }}</mat-label>\n <textarea\n matInput\n rows=\"3\"\n [ngModel]=\"paletteValue()\"\n (ngModelChange)=\"setPalette($event)\"\n [disabled]=\"isReadonly()\"\n ></textarea>\n </mat-form-field>\n }\n\n <p class=\"editor-caption\">\n {{ t('praxis.charts.editor.appearance.paletteHint', 'Use a registered token or comma-separated colors to persist theme.palette in the canonical contract.') }}\n </p>\n </mat-card-content>\n </mat-card>\n\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.appearance.surfaceTitle', 'Surface') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content>\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.surfaceMode', 'Surface mode') }}</mat-label>\n <mat-select [ngModel]=\"surfaceModeValue()\" (ngModelChange)=\"setSurfaceMode($event)\" [disabled]=\"isReadonly()\">\n @for (mode of surfaceModes; track mode) {\n <mat-option [value]=\"mode\">\n {{ t('praxis.charts.editor.surface.' + mode, mode) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n <p class=\"editor-caption\">\n {{ t('praxis.charts.editor.appearance.surfaceHint', 'Use embedded for dashboard widgets and contained only when the chart must own its visual surface.') }}\n </p>\n </mat-card-content>\n </mat-card>\n\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.appearance.statesTitle', 'State messages') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-stack\">\n <div class=\"editor-row-card\">\n <div class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.emptyTitle', 'Empty title') }}</mat-label>\n <input matInput [ngModel]=\"stateTitle('empty')\" (ngModelChange)=\"setStateTitle('empty', $event)\" [disabled]=\"isReadonly()\" />\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.emptyDescription', 'Empty description') }}</mat-label>\n <textarea matInput rows=\"2\" [ngModel]=\"stateDescription('empty')\" (ngModelChange)=\"setStateDescription('empty', $event)\" [disabled]=\"isReadonly()\"></textarea>\n </mat-form-field>\n </div>\n </div>\n\n <div class=\"editor-row-card\">\n <div class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.loadingTitle', 'Loading title') }}</mat-label>\n <input matInput [ngModel]=\"stateTitle('loading')\" (ngModelChange)=\"setStateTitle('loading', $event)\" [disabled]=\"isReadonly()\" />\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.loadingDescription', 'Loading description') }}</mat-label>\n <textarea matInput rows=\"2\" [ngModel]=\"stateDescription('loading')\" (ngModelChange)=\"setStateDescription('loading', $event)\" [disabled]=\"isReadonly()\"></textarea>\n </mat-form-field>\n </div>\n </div>\n\n <div class=\"editor-row-card\">\n <div class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.errorTitle', 'Error title') }}</mat-label>\n <input matInput [ngModel]=\"stateTitle('error')\" (ngModelChange)=\"setStateTitle('error', $event)\" [disabled]=\"isReadonly()\" />\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.errorDescription', 'Error description') }}</mat-label>\n <textarea matInput rows=\"2\" [ngModel]=\"stateDescription('error')\" (ngModelChange)=\"setStateDescription('error', $event)\" [disabled]=\"isReadonly()\"></textarea>\n </mat-form-field>\n </div>\n </div>\n </mat-card-content>\n </mat-card>\n </div>\n }\n\n @case ('analytics') {\n <div class=\"editor-stack\">\n @if (showComboPanel()) {\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.specialization.comboTitle', 'Combo guidance') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content>\n <p class=\"editor-caption\">\n {{ t('praxis.charts.editor.specialization.comboHint', 'Combo charts require at least two metrics and allow per-metric axis and series kind mapping.') }}\n </p>\n </mat-card-content>\n </mat-card>\n }\n\n @if (showPieDonutPanel()) {\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.specialization.pieDonutTitle', 'Composition guidance') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content>\n <p class=\"editor-caption\">\n {{ t('praxis.charts.editor.specialization.pieDonutHint', 'Pie and donut charts keep only the first metric and use the first dimension as the category segment.') }}\n </p>\n </mat-card-content>\n </mat-card>\n }\n\n @if (showScatterPanel()) {\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.specialization.scatterTitle', 'Scatter guidance') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content>\n <p class=\"editor-caption\">\n {{ t('praxis.charts.editor.specialization.scatterHint', 'Scatter charts use the first dimension as X and the first metric as Y, so keep both fields mapped.') }}\n </p>\n </mat-card-content>\n </mat-card>\n }\n\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.analytics.dimensionsTitle', 'Dimensions') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-stack\">\n @for (dimension of dimensions(); track $index) {\n <div class=\"editor-row-card\" [attr.data-testid]=\"'chart-editor-dimension-row-' + $index\">\n <div class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\" [attr.data-testid]=\"'chart-editor-dimension-field-' + $index\">\n <mat-label>{{ t('praxis.charts.editor.field.dimension', 'Dimension') }}</mat-label>\n @if (fieldOptions('dimension').length) {\n <mat-select [ngModel]=\"dimension.field || ''\" (ngModelChange)=\"setDimensionField($index, $event)\" [disabled]=\"isReadonly()\">\n @for (field of fieldOptions('dimension'); track field.field) {\n <mat-option [value]=\"field.field\">{{ fieldOptionLabel(field) }}</mat-option>\n }\n </mat-select>\n } @else {\n <mat-select [ngModel]=\"dimension.field || ''\" (ngModelChange)=\"setDimensionField($index, $event)\" [disabled]=\"true\"></mat-select>\n <mat-hint>{{ t('praxis.charts.editor.catalog.dimensionFieldsMissing', 'No governed dimensions are available for the current resource and operation.') }}</mat-hint>\n }\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\" [attr.data-testid]=\"'chart-editor-dimension-role-' + $index\">\n <mat-label>{{ t('praxis.charts.editor.field.dimensionRole', 'Dimension role') }}</mat-label>\n <mat-select [ngModel]=\"dimension.role || 'category'\" (ngModelChange)=\"setDimensionRole($index, $event)\" [disabled]=\"isReadonly()\">\n @for (role of dimensionRoles; track role) {\n <mat-option [value]=\"role\">\n {{ t('praxis.charts.editor.dimensionRole.' + role, role) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n </div>\n\n <div class=\"editor-row-actions\">\n <button mat-button type=\"button\" (click)=\"removeDimension($index)\" [disabled]=\"isReadonly() || dimensions().length <= 1\">\n {{ t('praxis.charts.editor.analytics.removeDimension', 'Remove dimension') }}\n </button>\n </div>\n </div>\n }\n\n <div>\n <button mat-stroked-button type=\"button\" data-testid=\"chart-editor-add-dimension\" (click)=\"addDimension()\" [disabled]=\"isReadonly()\">\n {{ t('praxis.charts.editor.analytics.addDimension', 'Add dimension') }}\n </button>\n </div>\n </mat-card-content>\n </mat-card>\n\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.analytics.metricsTitle', 'Metrics') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-stack\">\n @for (metric of metrics(); track $index) {\n <div class=\"editor-row-card\" [attr.data-testid]=\"'chart-editor-metric-row-' + $index\">\n <div class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\" [attr.data-testid]=\"'chart-editor-metric-field-' + $index\">\n <mat-label>{{ t('praxis.charts.editor.field.metric', 'Metric') }}</mat-label>\n @if (fieldOptions('metric').length) {\n <mat-select [ngModel]=\"metric.field || ''\" (ngModelChange)=\"setMetricField($index, $event)\" [disabled]=\"isReadonly()\">\n @for (field of fieldOptions('metric'); track field.field) {\n <mat-option [value]=\"field.field\">{{ fieldOptionLabel(field) }}</mat-option>\n }\n </mat-select>\n } @else {\n <mat-select [ngModel]=\"metric.field || ''\" (ngModelChange)=\"setMetricField($index, $event)\" [disabled]=\"true\"></mat-select>\n <mat-hint>{{ t('praxis.charts.editor.catalog.metricFieldsMissing', 'No governed metrics are available for the current resource and operation.') }}</mat-hint>\n }\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\" [attr.data-testid]=\"'chart-editor-metric-label-' + $index\">\n <mat-label>{{ t('praxis.charts.editor.field.metricLabel', 'Metric label') }}</mat-label>\n <input matInput [ngModel]=\"metric.label || ''\" (ngModelChange)=\"setMetricLabel($index, $event)\" [disabled]=\"isReadonly()\" />\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\" [attr.data-testid]=\"'chart-editor-metric-aggregation-' + $index\">\n <mat-label>{{ t('praxis.charts.editor.field.metricAggregation', 'Aggregation') }}</mat-label>\n <mat-select [ngModel]=\"metric.aggregation || 'sum'\" (ngModelChange)=\"setMetricAggregation($index, $event)\" [disabled]=\"isReadonly()\">\n @for (aggregation of metricAggregationOptions(metric.field); track aggregation) {\n <mat-option [value]=\"aggregation\">\n {{ t('praxis.charts.editor.metricAggregation.' + aggregation, aggregation) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n @if (showMetricAxisControls()) {\n <mat-form-field class=\"editor-field\" appearance=\"outline\" [attr.data-testid]=\"'chart-editor-metric-axis-' + $index\">\n <mat-label>{{ t('praxis.charts.editor.field.metricAxis', 'Axis') }}</mat-label>\n <mat-select [ngModel]=\"metric.axis || 'primary'\" (ngModelChange)=\"setMetricAxis($index, $event)\" [disabled]=\"isReadonly()\">\n @for (axis of metricAxes; track axis) {\n <mat-option [value]=\"axis\">\n {{ t('praxis.charts.editor.metricAxis.' + axis, axis) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n }\n\n @if (showMetricSeriesKindControls()) {\n <mat-form-field class=\"editor-field\" appearance=\"outline\" [attr.data-testid]=\"'chart-editor-metric-series-kind-' + $index\">\n <mat-label>{{ t('praxis.charts.editor.field.metricSeriesKind', 'Series kind') }}</mat-label>\n <mat-select [ngModel]=\"metric.seriesKind || 'bar'\" (ngModelChange)=\"setMetricSeriesKind($index, $event)\" [disabled]=\"isReadonly()\">\n @for (seriesKind of metricSeriesKinds; track seriesKind) {\n <mat-option [value]=\"seriesKind\">\n {{ t('praxis.charts.editor.metricSeriesKind.' + seriesKind, seriesKind) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n }\n </div>\n\n <div class=\"editor-row-actions\">\n <button mat-button type=\"button\" [attr.data-testid]=\"'chart-editor-remove-metric-' + $index\" (click)=\"removeMetric($index)\" [disabled]=\"isReadonly() || metrics().length <= 1\">\n {{ t('praxis.charts.editor.analytics.removeMetric', 'Remove metric') }}\n </button>\n </div>\n </div>\n }\n\n <div>\n <button mat-stroked-button type=\"button\" data-testid=\"chart-editor-add-metric\" (click)=\"addMetric()\" [disabled]=\"isReadonly()\">\n {{ t('praxis.charts.editor.analytics.addMetric', 'Add metric') }}\n </button>\n </div>\n </mat-card-content>\n </mat-card>\n </div>\n }\n\n @case ('events') {\n <div class=\"editor-stack\">\n <mat-card class=\"editor-card\" data-testid=\"chart-editor-event-pointClick\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.events.pointClickTitle', 'Point click') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventAction', 'Action') }}</mat-label>\n <mat-select data-testid=\"chart-editor-event-pointClick-action\" [ngModel]=\"eventAction('pointClick')\" (ngModelChange)=\"setEventAction('pointClick', $event)\" [disabled]=\"isReadonly()\">\n <mat-option value=\"\">{{ t('praxis.charts.editor.events.none', 'None') }}</mat-option>\n @for (action of eventActionOptions; track action) {\n <mat-option [value]=\"action\">\n {{ t('praxis.charts.editor.eventAction.' + action, action) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventTarget', 'Target') }}</mat-label>\n <mat-select data-testid=\"chart-editor-event-pointClick-target\" [ngModel]=\"eventTarget('pointClick')\" (ngModelChange)=\"setEventTarget('pointClick', $event)\" [disabled]=\"isReadonly() || !eventAction('pointClick') || targetCatalogUnavailable(eventAction('pointClick'), 'pointClick')\">\n @for (target of targetOptions(eventAction('pointClick'), 'pointClick'); track target.id) {\n <mat-option [value]=\"target.id\">{{ target.label }}</mat-option>\n }\n </mat-select>\n @if (targetCatalogUnavailable(eventAction('pointClick'), 'pointClick')) {\n <mat-hint>{{ t('praxis.charts.editor.catalog.targetMissing', 'Target catalog is required for this governed event action.') }}</mat-hint>\n }\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventMapping', 'Mapping') }}</mat-label>\n <textarea\n matInput\n data-testid=\"chart-editor-event-pointClick-mapping\"\n rows=\"4\"\n [ngModel]=\"eventMappingText('pointClick')\"\n (ngModelChange)=\"setEventMapping('pointClick', $event)\"\n [disabled]=\"isReadonly() || !eventAction('pointClick')\"\n ></textarea>\n </mat-form-field>\n </mat-card-content>\n </mat-card>\n\n <mat-card class=\"editor-card\" data-testid=\"chart-editor-event-selectionChange\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.events.selectionChangeTitle', 'Selection change') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventAction', 'Action') }}</mat-label>\n <mat-select data-testid=\"chart-editor-event-selectionChange-action\" [ngModel]=\"eventAction('selectionChange')\" (ngModelChange)=\"setEventAction('selectionChange', $event)\" [disabled]=\"isReadonly()\">\n <mat-option value=\"\">{{ t('praxis.charts.editor.events.none', 'None') }}</mat-option>\n @for (action of eventActionOptions; track action) {\n <mat-option [value]=\"action\">\n {{ t('praxis.charts.editor.eventAction.' + action, action) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventTarget', 'Target') }}</mat-label>\n <mat-select data-testid=\"chart-editor-event-selectionChange-target\" [ngModel]=\"eventTarget('selectionChange')\" (ngModelChange)=\"setEventTarget('selectionChange', $event)\" [disabled]=\"isReadonly() || !eventAction('selectionChange') || targetCatalogUnavailable(eventAction('selectionChange'), 'selectionChange')\">\n @for (target of targetOptions(eventAction('selectionChange'), 'selectionChange'); track target.id) {\n <mat-option [value]=\"target.id\">{{ target.label }}</mat-option>\n }\n </mat-select>\n @if (targetCatalogUnavailable(eventAction('selectionChange'), 'selectionChange')) {\n <mat-hint>{{ t('praxis.charts.editor.catalog.targetMissing', 'Target catalog is required for this governed event action.') }}</mat-hint>\n }\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventMapping', 'Mapping') }}</mat-label>\n <textarea\n matInput\n data-testid=\"chart-editor-event-selectionChange-mapping\"\n rows=\"4\"\n [ngModel]=\"eventMappingText('selectionChange')\"\n (ngModelChange)=\"setEventMapping('selectionChange', $event)\"\n [disabled]=\"isReadonly() || !eventAction('selectionChange')\"\n ></textarea>\n </mat-form-field>\n </mat-card-content>\n </mat-card>\n\n <mat-card class=\"editor-card\" data-testid=\"chart-editor-event-drillDown\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.events.drillDownTitle', 'Drill down') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventAction', 'Action') }}</mat-label>\n <mat-select data-testid=\"chart-editor-event-drillDown-action\" [ngModel]=\"eventAction('drillDown')\" (ngModelChange)=\"setEventAction('drillDown', $event)\" [disabled]=\"isReadonly()\">\n <mat-option value=\"\">{{ t('praxis.charts.editor.events.none', 'None') }}</mat-option>\n @for (action of eventActionOptions; track action) {\n <mat-option [value]=\"action\">\n {{ t('praxis.charts.editor.eventAction.' + action, action) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventTarget', 'Target') }}</mat-label>\n <mat-select data-testid=\"chart-editor-event-drillDown-target\" [ngModel]=\"eventTarget('drillDown')\" (ngModelChange)=\"setEventTarget('drillDown', $event)\" [disabled]=\"isReadonly() || !eventAction('drillDown') || targetCatalogUnavailable(eventAction('drillDown'), 'drillDown')\">\n @for (target of targetOptions(eventAction('drillDown'), 'drillDown'); track target.id) {\n <mat-option [value]=\"target.id\">{{ target.label }}</mat-option>\n }\n </mat-select>\n @if (targetCatalogUnavailable(eventAction('drillDown'), 'drillDown')) {\n <mat-hint>{{ t('praxis.charts.editor.catalog.targetMissing', 'Target catalog is required for this governed event action.') }}</mat-hint>\n }\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventMapping', 'Mapping') }}</mat-label>\n <textarea\n matInput\n data-testid=\"chart-editor-event-drillDown-mapping\"\n rows=\"4\"\n [ngModel]=\"eventMappingText('drillDown')\"\n (ngModelChange)=\"setEventMapping('drillDown', $event)\"\n [disabled]=\"isReadonly() || !eventAction('drillDown')\"\n ></textarea>\n </mat-form-field>\n </mat-card-content>\n </mat-card>\n\n <mat-card class=\"editor-card\" data-testid=\"chart-editor-event-crossFilter\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.events.crossFilterTitle', 'Cross filter') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventAction', 'Action') }}</mat-label>\n <mat-select data-testid=\"chart-editor-event-crossFilter-action\" [ngModel]=\"eventAction('crossFilter')\" (ngModelChange)=\"setEventAction('crossFilter', $event)\" [disabled]=\"isReadonly()\">\n <mat-option value=\"\">{{ t('praxis.charts.editor.events.none', 'None') }}</mat-option>\n @for (action of eventActionOptions; track action) {\n <mat-option [value]=\"action\">\n {{ t('praxis.charts.editor.eventAction.' + action, action) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventTarget', 'Target') }}</mat-label>\n <mat-select data-testid=\"chart-editor-event-crossFilter-target\" [ngModel]=\"eventTarget('crossFilter')\" (ngModelChange)=\"setEventTarget('crossFilter', $event)\" [disabled]=\"isReadonly() || !eventAction('crossFilter') || targetCatalogUnavailable(eventAction('crossFilter'), 'crossFilter')\">\n @for (target of targetOptions(eventAction('crossFilter'), 'crossFilter'); track target.id) {\n <mat-option [value]=\"target.id\">{{ target.label }}</mat-option>\n }\n </mat-select>\n @if (targetCatalogUnavailable(eventAction('crossFilter'), 'crossFilter')) {\n <mat-hint>{{ t('praxis.charts.editor.catalog.targetMissing', 'Target catalog is required for this governed event action.') }}</mat-hint>\n }\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventMapping', 'Mapping') }}</mat-label>\n <textarea\n matInput\n data-testid=\"chart-editor-event-crossFilter-mapping\"\n rows=\"4\"\n [ngModel]=\"eventMappingText('crossFilter')\"\n (ngModelChange)=\"setEventMapping('crossFilter', $event)\"\n [disabled]=\"isReadonly() || !eventAction('crossFilter')\"\n ></textarea>\n </mat-form-field>\n </mat-card-content>\n </mat-card>\n </div>\n }\n\n @case ('preview') {\n <p class=\"editor-caption\">\n {{ t('praxis.charts.editor.preview.caption', 'Local preview derived from the canonical contract without remote calls.') }}\n </p>\n }\n }\n </mat-card-content>\n </mat-card>\n </div>\n\n <div class=\"editor-side\">\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.issues.title', 'Validation issues') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content>\n @if (issues().length) {\n <ul class=\"editor-issues\">\n @for (issue of issues(); track issueTrackBy($index, issue)) {\n <li class=\"editor-issue\">\n <strong>{{ issue.field }}</strong>\n <span>{{ issue.message }}</span>\n </li>\n }\n </ul>\n } @else {\n <div class=\"editor-empty\">\n {{ t('praxis.charts.editor.issues.empty', 'No issues were identified.') }}\n </div>\n }\n </mat-card-content>\n </mat-card>\n\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.preview.title', 'Chart preview') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content>\n @if (preview(); as chartPreview) {\n <p class=\"editor-caption\">\n {{ t('praxis.charts.editor.preview.caption', 'Local preview derived from the canonical contract without remote calls.') }}\n </p>\n <praxis-chart [config]=\"chartPreview.config\" [data]=\"chartPreview.data\"></praxis-chart>\n } @else {\n <div class=\"editor-empty\">\n {{ t('praxis.charts.editor.preview.invalid', 'Preview is unavailable while the contract has blocking errors.') }}\n </div>\n }\n </mat-card-content>\n </mat-card>\n </div>\n </div>\n</div>\n", styles: [":host{display:block;min-width:0;container:praxis-chart-editor / inline-size;color:var(--md-sys-color-on-surface, #1a1b20)}.editor-shell{display:grid;gap:18px;min-width:0}.editor-nav{display:flex;gap:8px;flex-wrap:wrap}.editor-nav button.active{background:color-mix(in srgb,var(--md-sys-color-primary, #1263b4) 18%,transparent);color:var(--md-sys-color-primary, #1263b4)}.editor-layout{display:grid;gap:18px;grid-template-columns:minmax(0,1.35fr) minmax(320px,.9fr);align-items:start}.editor-form,.editor-side{display:grid;gap:16px;min-width:0}.editor-card{box-sizing:border-box;width:100%;min-width:0;border-radius:20px;border:1px solid color-mix(in srgb,var(--md-sys-color-outline, #c5c7ce) 72%,transparent);background:linear-gradient(180deg,#1263b408,#1263b400)}.editor-grid{display:grid;gap:14px;grid-template-columns:repeat(auto-fit,minmax(220px,1fr))}.editor-stack{display:grid;gap:14px}.editor-row-card{padding:14px;border-radius:16px;border:1px solid color-mix(in srgb,var(--md-sys-color-outline, #c5c7ce) 54%,transparent);background:color-mix(in srgb,var(--md-sys-color-surface, #fff) 92%,rgba(18,99,180,.04))}.editor-row-actions{display:flex;justify-content:flex-end}.editor-field{width:100%}.editor-issues{display:grid;gap:10px;margin:0;padding:0;list-style:none}.editor-issue{padding:12px 14px;border-radius:14px;background:color-mix(in srgb,var(--md-sys-color-error, #b3261e) 8%,transparent);border:1px solid color-mix(in srgb,var(--md-sys-color-error, #b3261e) 18%,transparent)}.editor-issue strong{display:block;margin-bottom:4px}.editor-caption{margin:0 0 12px;color:var(--md-sys-color-on-surface-variant, #5a5d67);font-size:.92rem}.editor-empty{padding:18px;border-radius:14px;background:color-mix(in srgb,var(--md-sys-color-surface-variant, #eceff4) 78%,transparent)}.editor-side praxis-chart{display:block;width:100%;min-width:0;max-width:100%}@container praxis-chart-editor (max-width: 840px){.editor-layout{grid-template-columns:minmax(0,1fr)}}@media(max-width:960px){.editor-layout{grid-template-columns:minmax(0,1fr)}}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$3.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$3.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$3.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i1$2.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: MatCardModule }, { kind: "component", type: i3$1.MatCard, selector: "mat-card", inputs: ["appearance"], exportAs: ["matCard"] }, { kind: "directive", type: i3$1.MatCardContent, selector: "mat-card-content" }, { kind: "component", type: i3$1.MatCardHeader, selector: "mat-card-header" }, { kind: "directive", type: i3$1.MatCardTitle, selector: "mat-card-title, [mat-card-title], [matCardTitle]" }, { kind: "ngmodule", type: MatFormFieldModule }, { kind: "component", type: i4.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i4.MatLabel, selector: "mat-label" }, { kind: "directive", type: i4.MatHint, selector: "mat-hint", inputs: ["align", "id"] }, { kind: "ngmodule", type: MatInputModule }, { kind: "directive", type: i5.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["disabled", "id", "placeholder", "name", "required", "type", "errorStateMatcher", "aria-describedby", "value", "readonly", "disabledInteractive"], exportAs: ["matInput"] }, { kind: "ngmodule", type: MatSelectModule }, { kind: "component", type: i6.MatSelect, selector: "mat-select", inputs: ["aria-describedby", "panelClass", "disabled", "disableRipple", "tabIndex", "hideSingleSelectionIndicator", "placeholder", "required", "multiple", "disableOptionCentering", "compareWith", "value", "aria-label", "aria-labelledby", "errorStateMatcher", "typeaheadDebounceInterval", "sortComparator", "id", "panelWidth", "canSelectNullableOptions"], outputs: ["openedChange", "opened", "closed", "selectionChange", "valueChange"], exportAs: ["matSelect"] }, { kind: "component", type: i6.MatOption, selector: "mat-option", inputs: ["value", "id", "disabled"], outputs: ["onSelectionChange"], exportAs: ["matOption"] }, { kind: "ngmodule", type: MatSlideToggleModule }, { kind: "component", type: i7.MatSlideToggle, selector: "mat-slide-toggle", inputs: ["name", "id", "labelPosition", "aria-label", "aria-labelledby", "aria-describedby", "required", "color", "disabled", "disableRipple", "tabIndex", "checked", "hideIcon", "disabledInteractive"], outputs: ["change", "toggleChange"], exportAs: ["matSlideToggle"] }, { kind: "component", type: PraxisChartComponent, selector: "praxis-chart", inputs: ["config", "data", "chartDocument", "filterCriteria", "queryContext", "remoteDataResolver", "enableCustomization", "availableResources", "availableFields", "availableTargets"], outputs: ["pointClick", "pointAction", "selectionChange", "drillDown", "crossFilter", "queryRequest", "loadStateChange", "chartDocumentApplied", "chartDocumentSaved"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
5210
6840
|
}
|
|
5211
6841
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: PraxisChartConfigEditor, decorators: [{
|
|
5212
6842
|
type: Component,
|
|
@@ -5219,7 +6849,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
|
|
|
5219
6849
|
MatSelectModule,
|
|
5220
6850
|
MatSlideToggleModule,
|
|
5221
6851
|
PraxisChartComponent
|
|
5222
|
-
], providers: [providePraxisChartsI18n()], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"editor-shell\">\n <div class=\"editor-nav\">\n @for (section of sections; track section.id) {\n <button\n mat-stroked-button\n type=\"button\"\n [class.active]=\"activeSection() === section.id\"\n (click)=\"setSection(section.id)\"\n >\n {{ t(section.labelKey, section.fallback) }}\n </button>\n }\n </div>\n\n <div class=\"editor-layout\">\n <div class=\"editor-form\">\n <mat-card class=\"editor-card\">\n <mat-card-content>\n @switch (activeSection()) {\n @case ('general') {\n <div class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.chartId', 'Chart ID') }}</mat-label>\n <input matInput [ngModel]=\"doc().chartId || ''\" (ngModelChange)=\"setChartId($event)\" [disabled]=\"isReadonly()\" />\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.kind', 'Kind') }}</mat-label>\n <mat-select [ngModel]=\"doc().kind\" (ngModelChange)=\"setKind($event)\" [disabled]=\"isReadonly()\">\n @for (kind of chartKinds; track kind) {\n <mat-option [value]=\"kind\">\n {{ t('praxis.charts.editor.kind.' + kind, kind) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.title', 'Title') }}</mat-label>\n <input matInput data-testid=\"chart-editor-title-input\" [ngModel]=\"titleValue()\" (ngModelChange)=\"setTitle($event)\" [disabled]=\"isReadonly()\" />\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.subtitle', 'Subtitle') }}</mat-label>\n <input matInput [ngModel]=\"subtitleValue()\" (ngModelChange)=\"setSubtitle($event)\" [disabled]=\"isReadonly()\" />\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.sizingMode', 'Sizing') }}</mat-label>\n <mat-select [ngModel]=\"sizingModeValue()\" (ngModelChange)=\"setSizingMode($event)\" [disabled]=\"isReadonly()\">\n @for (mode of sizingModes; track mode) {\n <mat-option [value]=\"mode\">\n {{ t('praxis.charts.editor.sizing.' + mode, mode) }}\n </mat-option>\n }\n </mat-select>\n <mat-hint>{{ t('praxis.charts.editor.hint.sizingMode', 'Use fill-container only when the host widget provides a defined body height.') }}</mat-hint>\n </mat-form-field>\n\n @if (sizingModeValue() === 'fixed') {\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.height', 'Height') }}</mat-label>\n <input matInput [ngModel]=\"heightValue()\" (ngModelChange)=\"setSizingHeight($event)\" [disabled]=\"isReadonly()\" />\n <mat-hint>{{ t('praxis.charts.editor.hint.height', 'Numbers are saved as pixels; CSS lengths such as 20rem are also accepted.') }}</mat-hint>\n </mat-form-field>\n }\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.minHeight', 'Minimum height') }}</mat-label>\n <input matInput [ngModel]=\"sizingMinHeightValue()\" (ngModelChange)=\"setSizingMinHeight($event)\" [disabled]=\"isReadonly()\" />\n <mat-hint>{{ t('praxis.charts.editor.hint.minHeight', 'Set a readable minimum for compact dashboard widgets.') }}</mat-hint>\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.maxHeight', 'Maximum height') }}</mat-label>\n <input matInput [ngModel]=\"sizingMaxHeightValue()\" (ngModelChange)=\"setSizingMaxHeight($event)\" [disabled]=\"isReadonly()\" />\n <mat-hint>{{ t('praxis.charts.editor.hint.maxHeight', 'Leave empty unless the chart must stop growing inside a flexible layout.') }}</mat-hint>\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.aspectRatio', 'Aspect ratio') }}</mat-label>\n <input matInput [ngModel]=\"sizingAspectRatioValue()\" (ngModelChange)=\"setSizingAspectRatio($event)\" [disabled]=\"isReadonly()\" />\n <mat-hint>{{ t('praxis.charts.editor.hint.aspectRatio', 'Optional. Use values such as 1.777 or 16 / 9.') }}</mat-hint>\n </mat-form-field>\n </div>\n }\n\n @case ('data') {\n <div class=\"editor-stack\">\n <div class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.sourceKind', 'Source') }}</mat-label>\n <mat-select [ngModel]=\"doc().source.kind\" (ngModelChange)=\"setSourceKind($event)\" [disabled]=\"isReadonly()\">\n @for (sourceKind of sourceKinds; track sourceKind) {\n <mat-option [value]=\"sourceKind\">\n {{ t('praxis.charts.editor.sourceKind.' + (sourceKind === 'praxis.stats' ? 'praxisStats' : 'derived'), sourceKind) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n @if (doc().source.kind === 'praxis.stats') {\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.resource', 'Resource') }}</mat-label>\n @if (resourceOptions().length) {\n <mat-select [ngModel]=\"resourceValue()\" (ngModelChange)=\"setResource($event)\" [disabled]=\"isReadonly()\">\n @for (resource of resourceOptions(); track resource.id) {\n <mat-option [value]=\"resource.path\">{{ resource.label }}</mat-option>\n }\n </mat-select>\n } @else {\n <input matInput [ngModel]=\"resourceValue()\" (ngModelChange)=\"setResource($event)\" [disabled]=\"isReadonly()\" />\n }\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.operation', 'Operation') }}</mat-label>\n <mat-select\n [ngModel]=\"doc().source.operation || 'group-by'\"\n (ngModelChange)=\"setOperation($event)\"\n [disabled]=\"isReadonly()\"\n >\n @for (operation of operations; track operation) {\n <mat-option [value]=\"operation\">\n {{ t('praxis.charts.editor.operation.' + (operation === 'group-by' ? 'groupBy' : operation), operation) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n }\n </div>\n\n @if (showTimeseriesControls()) {\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.specialization.timeseriesTitle', 'Timeseries options') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.granularity', 'Granularity') }}</mat-label>\n <mat-select [ngModel]=\"granularityValue()\" (ngModelChange)=\"setGranularity($event)\" [disabled]=\"isReadonly()\">\n @for (granularity of timeGranularities; track granularity) {\n <mat-option [value]=\"granularity\">\n {{ t('praxis.charts.editor.granularity.' + granularity, granularity) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n <mat-slide-toggle\n [ngModel]=\"fillGapsValue()\"\n (ngModelChange)=\"setFillGaps($event)\"\n [disabled]=\"isReadonly()\"\n >\n {{ t('praxis.charts.editor.field.fillGaps', 'Fill missing intervals') }}\n </mat-slide-toggle>\n </mat-card-content>\n </mat-card>\n }\n\n @if (showDistributionControls()) {\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.specialization.distributionTitle', 'Distribution options') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.distributionMode', 'Distribution mode') }}</mat-label>\n <mat-select [ngModel]=\"distributionModeValue()\" (ngModelChange)=\"setDistributionMode($event)\" [disabled]=\"isReadonly()\">\n @for (mode of distributionModes; track mode) {\n <mat-option [value]=\"mode\">\n {{ t('praxis.charts.editor.distributionMode.' + mode, mode) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.bucketSize', 'Bucket size') }}</mat-label>\n <input matInput [ngModel]=\"bucketSizeValue()\" (ngModelChange)=\"setBucketSize($event)\" [disabled]=\"isReadonly()\" />\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.bucketCount', 'Bucket count') }}</mat-label>\n <input matInput [ngModel]=\"bucketCountValue()\" (ngModelChange)=\"setBucketCount($event)\" [disabled]=\"isReadonly()\" />\n </mat-form-field>\n </mat-card-content>\n </mat-card>\n }\n </div>\n }\n\n @case ('motion') {\n <div class=\"editor-grid\">\n <mat-slide-toggle\n [ngModel]=\"normalizedDocument().motion?.enabled !== false\"\n (ngModelChange)=\"setMotionEnabled($event)\"\n [disabled]=\"isReadonly()\"\n >\n {{ t('praxis.charts.editor.field.motionEnabled', 'Enable animations') }}\n </mat-slide-toggle>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.motionPreset', 'Motion preset') }}</mat-label>\n <mat-select\n [ngModel]=\"normalizedDocument().motion?.preset || 'standard'\"\n (ngModelChange)=\"setMotionPreset($event)\"\n [disabled]=\"isReadonly() || normalizedDocument().motion?.enabled === false\"\n >\n @for (preset of motionPresets; track preset) {\n <mat-option [value]=\"preset\">\n {{ t('praxis.charts.editor.motionPreset.' + preset, preset) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n </div>\n }\n\n @case ('appearance') {\n <div class=\"editor-stack\">\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.appearance.featuresTitle', 'Display features') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-grid\">\n <mat-slide-toggle\n [ngModel]=\"featureEnabled('legend')\"\n (ngModelChange)=\"setFeatureEnabled('legend', $event)\"\n [disabled]=\"isReadonly()\"\n >\n {{ t('praxis.charts.editor.field.legendEnabled', 'Show legend') }}\n </mat-slide-toggle>\n\n <mat-slide-toggle\n [ngModel]=\"featureEnabled('labels')\"\n (ngModelChange)=\"setFeatureEnabled('labels', $event)\"\n [disabled]=\"isReadonly()\"\n >\n {{ t('praxis.charts.editor.field.labelsEnabled', 'Show labels') }}\n </mat-slide-toggle>\n\n <mat-slide-toggle\n [ngModel]=\"featureEnabled('tooltip')\"\n (ngModelChange)=\"setFeatureEnabled('tooltip', $event)\"\n [disabled]=\"isReadonly()\"\n >\n {{ t('praxis.charts.editor.field.tooltipEnabled', 'Show tooltip') }}\n </mat-slide-toggle>\n </mat-card-content>\n </mat-card>\n\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.appearance.paletteTitle', 'Palette') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-stack\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.themeVariant', 'Theme variant') }}</mat-label>\n <mat-select [ngModel]=\"themeVariantValue()\" (ngModelChange)=\"setThemeVariant($event)\" [disabled]=\"isReadonly()\">\n <mat-option [value]=\"''\">\n {{ t('praxis.charts.editor.themeVariant.none', 'No variant') }}\n </mat-option>\n @for (variant of themeVariants; track variant) {\n <mat-option [value]=\"variant\">\n {{ t('praxis.charts.editor.themeVariant.' + variant, variant) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.paletteMode', 'Palette mode') }}</mat-label>\n <mat-select [ngModel]=\"paletteModeValue()\" (ngModelChange)=\"setPaletteMode($event)\" [disabled]=\"isReadonly()\">\n @for (mode of paletteModes; track mode) {\n <mat-option [value]=\"mode\">\n {{ t('praxis.charts.editor.paletteMode.' + mode, mode) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n @if (paletteModeValue() === 'token') {\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.paletteToken', 'Palette token') }}</mat-label>\n <mat-select [ngModel]=\"paletteTokenValue()\" (ngModelChange)=\"setPaletteToken($event)\" [disabled]=\"isReadonly()\">\n @for (token of paletteTokens; track token) {\n <mat-option [value]=\"token\">\n {{ t('praxis.charts.editor.paletteToken.' + token, token) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n } @else {\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.palette', 'Palette colors') }}</mat-label>\n <textarea\n matInput\n rows=\"3\"\n [ngModel]=\"paletteValue()\"\n (ngModelChange)=\"setPalette($event)\"\n [disabled]=\"isReadonly()\"\n ></textarea>\n </mat-form-field>\n }\n\n <p class=\"editor-caption\">\n {{ t('praxis.charts.editor.appearance.paletteHint', 'Use a registered token or comma-separated colors to persist theme.palette in the canonical contract.') }}\n </p>\n </mat-card-content>\n </mat-card>\n\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.appearance.surfaceTitle', 'Surface') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content>\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.surfaceMode', 'Surface mode') }}</mat-label>\n <mat-select [ngModel]=\"surfaceModeValue()\" (ngModelChange)=\"setSurfaceMode($event)\" [disabled]=\"isReadonly()\">\n @for (mode of surfaceModes; track mode) {\n <mat-option [value]=\"mode\">\n {{ t('praxis.charts.editor.surface.' + mode, mode) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n <p class=\"editor-caption\">\n {{ t('praxis.charts.editor.appearance.surfaceHint', 'Use embedded for dashboard widgets and contained only when the chart must own its visual surface.') }}\n </p>\n </mat-card-content>\n </mat-card>\n\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.appearance.statesTitle', 'State messages') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-stack\">\n <div class=\"editor-row-card\">\n <div class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.emptyTitle', 'Empty title') }}</mat-label>\n <input matInput [ngModel]=\"stateTitle('empty')\" (ngModelChange)=\"setStateTitle('empty', $event)\" [disabled]=\"isReadonly()\" />\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.emptyDescription', 'Empty description') }}</mat-label>\n <textarea matInput rows=\"2\" [ngModel]=\"stateDescription('empty')\" (ngModelChange)=\"setStateDescription('empty', $event)\" [disabled]=\"isReadonly()\"></textarea>\n </mat-form-field>\n </div>\n </div>\n\n <div class=\"editor-row-card\">\n <div class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.loadingTitle', 'Loading title') }}</mat-label>\n <input matInput [ngModel]=\"stateTitle('loading')\" (ngModelChange)=\"setStateTitle('loading', $event)\" [disabled]=\"isReadonly()\" />\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.loadingDescription', 'Loading description') }}</mat-label>\n <textarea matInput rows=\"2\" [ngModel]=\"stateDescription('loading')\" (ngModelChange)=\"setStateDescription('loading', $event)\" [disabled]=\"isReadonly()\"></textarea>\n </mat-form-field>\n </div>\n </div>\n\n <div class=\"editor-row-card\">\n <div class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.errorTitle', 'Error title') }}</mat-label>\n <input matInput [ngModel]=\"stateTitle('error')\" (ngModelChange)=\"setStateTitle('error', $event)\" [disabled]=\"isReadonly()\" />\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.errorDescription', 'Error description') }}</mat-label>\n <textarea matInput rows=\"2\" [ngModel]=\"stateDescription('error')\" (ngModelChange)=\"setStateDescription('error', $event)\" [disabled]=\"isReadonly()\"></textarea>\n </mat-form-field>\n </div>\n </div>\n </mat-card-content>\n </mat-card>\n </div>\n }\n\n @case ('analytics') {\n <div class=\"editor-stack\">\n @if (showComboPanel()) {\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.specialization.comboTitle', 'Combo guidance') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content>\n <p class=\"editor-caption\">\n {{ t('praxis.charts.editor.specialization.comboHint', 'Combo charts require at least two metrics and allow per-metric axis and series kind mapping.') }}\n </p>\n </mat-card-content>\n </mat-card>\n }\n\n @if (showPieDonutPanel()) {\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.specialization.pieDonutTitle', 'Composition guidance') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content>\n <p class=\"editor-caption\">\n {{ t('praxis.charts.editor.specialization.pieDonutHint', 'Pie and donut charts keep only the first metric and use the first dimension as the category segment.') }}\n </p>\n </mat-card-content>\n </mat-card>\n }\n\n @if (showScatterPanel()) {\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.specialization.scatterTitle', 'Scatter guidance') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content>\n <p class=\"editor-caption\">\n {{ t('praxis.charts.editor.specialization.scatterHint', 'Scatter charts use the first dimension as X and the first metric as Y, so keep both fields mapped.') }}\n </p>\n </mat-card-content>\n </mat-card>\n }\n\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.analytics.dimensionsTitle', 'Dimensions') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-stack\">\n @for (dimension of dimensions(); track $index) {\n <div class=\"editor-row-card\" [attr.data-testid]=\"'chart-editor-dimension-row-' + $index\">\n <div class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\" [attr.data-testid]=\"'chart-editor-dimension-field-' + $index\">\n <mat-label>{{ t('praxis.charts.editor.field.dimension', 'Dimension') }}</mat-label>\n @if (fieldOptions('dimension').length) {\n <mat-select [ngModel]=\"dimension.field || ''\" (ngModelChange)=\"setDimensionField($index, $event)\" [disabled]=\"isReadonly()\">\n @for (field of fieldOptions('dimension'); track field.field) {\n <mat-option [value]=\"field.field\">{{ field.label || field.field }}</mat-option>\n }\n </mat-select>\n } @else {\n <input matInput [ngModel]=\"dimension.field || ''\" (ngModelChange)=\"setDimensionField($index, $event)\" [disabled]=\"isReadonly()\" />\n }\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\" [attr.data-testid]=\"'chart-editor-dimension-role-' + $index\">\n <mat-label>{{ t('praxis.charts.editor.field.dimensionRole', 'Dimension role') }}</mat-label>\n <mat-select [ngModel]=\"dimension.role || 'category'\" (ngModelChange)=\"setDimensionRole($index, $event)\" [disabled]=\"isReadonly()\">\n @for (role of dimensionRoles; track role) {\n <mat-option [value]=\"role\">\n {{ t('praxis.charts.editor.dimensionRole.' + role, role) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n </div>\n\n <div class=\"editor-row-actions\">\n <button mat-button type=\"button\" (click)=\"removeDimension($index)\" [disabled]=\"isReadonly() || dimensions().length <= 1\">\n {{ t('praxis.charts.editor.analytics.removeDimension', 'Remove dimension') }}\n </button>\n </div>\n </div>\n }\n\n <div>\n <button mat-stroked-button type=\"button\" data-testid=\"chart-editor-add-dimension\" (click)=\"addDimension()\" [disabled]=\"isReadonly()\">\n {{ t('praxis.charts.editor.analytics.addDimension', 'Add dimension') }}\n </button>\n </div>\n </mat-card-content>\n </mat-card>\n\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.analytics.metricsTitle', 'Metrics') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-stack\">\n @for (metric of metrics(); track $index) {\n <div class=\"editor-row-card\" [attr.data-testid]=\"'chart-editor-metric-row-' + $index\">\n <div class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\" [attr.data-testid]=\"'chart-editor-metric-field-' + $index\">\n <mat-label>{{ t('praxis.charts.editor.field.metric', 'Metric') }}</mat-label>\n @if (fieldOptions('metric').length) {\n <mat-select [ngModel]=\"metric.field || ''\" (ngModelChange)=\"setMetricField($index, $event)\" [disabled]=\"isReadonly()\">\n @for (field of fieldOptions('metric'); track field.field) {\n <mat-option [value]=\"field.field\">{{ field.label || field.field }}</mat-option>\n }\n </mat-select>\n } @else {\n <input matInput [ngModel]=\"metric.field || ''\" (ngModelChange)=\"setMetricField($index, $event)\" [disabled]=\"isReadonly()\" />\n }\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\" [attr.data-testid]=\"'chart-editor-metric-label-' + $index\">\n <mat-label>{{ t('praxis.charts.editor.field.metricLabel', 'Metric label') }}</mat-label>\n <input matInput [ngModel]=\"metric.label || ''\" (ngModelChange)=\"setMetricLabel($index, $event)\" [disabled]=\"isReadonly()\" />\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\" [attr.data-testid]=\"'chart-editor-metric-aggregation-' + $index\">\n <mat-label>{{ t('praxis.charts.editor.field.metricAggregation', 'Aggregation') }}</mat-label>\n <mat-select [ngModel]=\"metric.aggregation || 'sum'\" (ngModelChange)=\"setMetricAggregation($index, $event)\" [disabled]=\"isReadonly()\">\n @for (aggregation of metricAggregations; track aggregation) {\n <mat-option [value]=\"aggregation\">\n {{ t('praxis.charts.editor.metricAggregation.' + aggregation, aggregation) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n @if (showMetricAxisControls()) {\n <mat-form-field class=\"editor-field\" appearance=\"outline\" [attr.data-testid]=\"'chart-editor-metric-axis-' + $index\">\n <mat-label>{{ t('praxis.charts.editor.field.metricAxis', 'Axis') }}</mat-label>\n <mat-select [ngModel]=\"metric.axis || 'primary'\" (ngModelChange)=\"setMetricAxis($index, $event)\" [disabled]=\"isReadonly()\">\n @for (axis of metricAxes; track axis) {\n <mat-option [value]=\"axis\">\n {{ t('praxis.charts.editor.metricAxis.' + axis, axis) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n }\n\n @if (showMetricSeriesKindControls()) {\n <mat-form-field class=\"editor-field\" appearance=\"outline\" [attr.data-testid]=\"'chart-editor-metric-series-kind-' + $index\">\n <mat-label>{{ t('praxis.charts.editor.field.metricSeriesKind', 'Series kind') }}</mat-label>\n <mat-select [ngModel]=\"metric.seriesKind || 'bar'\" (ngModelChange)=\"setMetricSeriesKind($index, $event)\" [disabled]=\"isReadonly()\">\n @for (seriesKind of metricSeriesKinds; track seriesKind) {\n <mat-option [value]=\"seriesKind\">\n {{ t('praxis.charts.editor.metricSeriesKind.' + seriesKind, seriesKind) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n }\n </div>\n\n <div class=\"editor-row-actions\">\n <button mat-button type=\"button\" [attr.data-testid]=\"'chart-editor-remove-metric-' + $index\" (click)=\"removeMetric($index)\" [disabled]=\"isReadonly() || metrics().length <= 1\">\n {{ t('praxis.charts.editor.analytics.removeMetric', 'Remove metric') }}\n </button>\n </div>\n </div>\n }\n\n <div>\n <button mat-stroked-button type=\"button\" data-testid=\"chart-editor-add-metric\" (click)=\"addMetric()\" [disabled]=\"isReadonly()\">\n {{ t('praxis.charts.editor.analytics.addMetric', 'Add metric') }}\n </button>\n </div>\n </mat-card-content>\n </mat-card>\n </div>\n }\n\n @case ('events') {\n <div class=\"editor-stack\">\n <mat-card class=\"editor-card\" data-testid=\"chart-editor-event-pointClick\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.events.pointClickTitle', 'Point click') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventAction', 'Action') }}</mat-label>\n <mat-select data-testid=\"chart-editor-event-pointClick-action\" [ngModel]=\"eventAction('pointClick')\" (ngModelChange)=\"setEventAction('pointClick', $event)\" [disabled]=\"isReadonly()\">\n <mat-option value=\"\">{{ t('praxis.charts.editor.events.none', 'None') }}</mat-option>\n @for (action of eventActionOptions; track action) {\n <mat-option [value]=\"action\">\n {{ t('praxis.charts.editor.eventAction.' + action, action) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventTarget', 'Target') }}</mat-label>\n @if (targetOptions().length) {\n <mat-select data-testid=\"chart-editor-event-pointClick-target\" [ngModel]=\"eventTarget('pointClick')\" (ngModelChange)=\"setEventTarget('pointClick', $event)\" [disabled]=\"isReadonly() || !eventAction('pointClick')\">\n @for (target of targetOptions(); track target.id) {\n <mat-option [value]=\"target.id\">{{ target.label }}</mat-option>\n }\n </mat-select>\n } @else {\n <input matInput data-testid=\"chart-editor-event-pointClick-target\" [ngModel]=\"eventTarget('pointClick')\" (ngModelChange)=\"setEventTarget('pointClick', $event)\" [disabled]=\"isReadonly() || !eventAction('pointClick')\" />\n }\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventMapping', 'Mapping') }}</mat-label>\n <textarea\n matInput\n data-testid=\"chart-editor-event-pointClick-mapping\"\n rows=\"4\"\n [ngModel]=\"eventMappingText('pointClick')\"\n (ngModelChange)=\"setEventMapping('pointClick', $event)\"\n [disabled]=\"isReadonly() || !eventAction('pointClick')\"\n ></textarea>\n </mat-form-field>\n </mat-card-content>\n </mat-card>\n\n <mat-card class=\"editor-card\" data-testid=\"chart-editor-event-selectionChange\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.events.selectionChangeTitle', 'Selection change') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventAction', 'Action') }}</mat-label>\n <mat-select data-testid=\"chart-editor-event-selectionChange-action\" [ngModel]=\"eventAction('selectionChange')\" (ngModelChange)=\"setEventAction('selectionChange', $event)\" [disabled]=\"isReadonly()\">\n <mat-option value=\"\">{{ t('praxis.charts.editor.events.none', 'None') }}</mat-option>\n @for (action of eventActionOptions; track action) {\n <mat-option [value]=\"action\">\n {{ t('praxis.charts.editor.eventAction.' + action, action) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventTarget', 'Target') }}</mat-label>\n @if (targetOptions().length) {\n <mat-select data-testid=\"chart-editor-event-selectionChange-target\" [ngModel]=\"eventTarget('selectionChange')\" (ngModelChange)=\"setEventTarget('selectionChange', $event)\" [disabled]=\"isReadonly() || !eventAction('selectionChange')\">\n @for (target of targetOptions(); track target.id) {\n <mat-option [value]=\"target.id\">{{ target.label }}</mat-option>\n }\n </mat-select>\n } @else {\n <input matInput data-testid=\"chart-editor-event-selectionChange-target\" [ngModel]=\"eventTarget('selectionChange')\" (ngModelChange)=\"setEventTarget('selectionChange', $event)\" [disabled]=\"isReadonly() || !eventAction('selectionChange')\" />\n }\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventMapping', 'Mapping') }}</mat-label>\n <textarea\n matInput\n data-testid=\"chart-editor-event-selectionChange-mapping\"\n rows=\"4\"\n [ngModel]=\"eventMappingText('selectionChange')\"\n (ngModelChange)=\"setEventMapping('selectionChange', $event)\"\n [disabled]=\"isReadonly() || !eventAction('selectionChange')\"\n ></textarea>\n </mat-form-field>\n </mat-card-content>\n </mat-card>\n\n <mat-card class=\"editor-card\" data-testid=\"chart-editor-event-drillDown\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.events.drillDownTitle', 'Drill down') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventAction', 'Action') }}</mat-label>\n <mat-select data-testid=\"chart-editor-event-drillDown-action\" [ngModel]=\"eventAction('drillDown')\" (ngModelChange)=\"setEventAction('drillDown', $event)\" [disabled]=\"isReadonly()\">\n <mat-option value=\"\">{{ t('praxis.charts.editor.events.none', 'None') }}</mat-option>\n @for (action of eventActionOptions; track action) {\n <mat-option [value]=\"action\">\n {{ t('praxis.charts.editor.eventAction.' + action, action) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventTarget', 'Target') }}</mat-label>\n @if (targetOptions().length) {\n <mat-select data-testid=\"chart-editor-event-drillDown-target\" [ngModel]=\"eventTarget('drillDown')\" (ngModelChange)=\"setEventTarget('drillDown', $event)\" [disabled]=\"isReadonly() || !eventAction('drillDown')\">\n @for (target of targetOptions(); track target.id) {\n <mat-option [value]=\"target.id\">{{ target.label }}</mat-option>\n }\n </mat-select>\n } @else {\n <input matInput data-testid=\"chart-editor-event-drillDown-target\" [ngModel]=\"eventTarget('drillDown')\" (ngModelChange)=\"setEventTarget('drillDown', $event)\" [disabled]=\"isReadonly() || !eventAction('drillDown')\" />\n }\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventMapping', 'Mapping') }}</mat-label>\n <textarea\n matInput\n data-testid=\"chart-editor-event-drillDown-mapping\"\n rows=\"4\"\n [ngModel]=\"eventMappingText('drillDown')\"\n (ngModelChange)=\"setEventMapping('drillDown', $event)\"\n [disabled]=\"isReadonly() || !eventAction('drillDown')\"\n ></textarea>\n </mat-form-field>\n </mat-card-content>\n </mat-card>\n\n <mat-card class=\"editor-card\" data-testid=\"chart-editor-event-crossFilter\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.events.crossFilterTitle', 'Cross filter') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventAction', 'Action') }}</mat-label>\n <mat-select data-testid=\"chart-editor-event-crossFilter-action\" [ngModel]=\"eventAction('crossFilter')\" (ngModelChange)=\"setEventAction('crossFilter', $event)\" [disabled]=\"isReadonly()\">\n <mat-option value=\"\">{{ t('praxis.charts.editor.events.none', 'None') }}</mat-option>\n @for (action of eventActionOptions; track action) {\n <mat-option [value]=\"action\">\n {{ t('praxis.charts.editor.eventAction.' + action, action) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventTarget', 'Target') }}</mat-label>\n @if (targetOptions().length) {\n <mat-select data-testid=\"chart-editor-event-crossFilter-target\" [ngModel]=\"eventTarget('crossFilter')\" (ngModelChange)=\"setEventTarget('crossFilter', $event)\" [disabled]=\"isReadonly() || !eventAction('crossFilter')\">\n @for (target of targetOptions(); track target.id) {\n <mat-option [value]=\"target.id\">{{ target.label }}</mat-option>\n }\n </mat-select>\n } @else {\n <input matInput data-testid=\"chart-editor-event-crossFilter-target\" [ngModel]=\"eventTarget('crossFilter')\" (ngModelChange)=\"setEventTarget('crossFilter', $event)\" [disabled]=\"isReadonly() || !eventAction('crossFilter')\" />\n }\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventMapping', 'Mapping') }}</mat-label>\n <textarea\n matInput\n data-testid=\"chart-editor-event-crossFilter-mapping\"\n rows=\"4\"\n [ngModel]=\"eventMappingText('crossFilter')\"\n (ngModelChange)=\"setEventMapping('crossFilter', $event)\"\n [disabled]=\"isReadonly() || !eventAction('crossFilter')\"\n ></textarea>\n </mat-form-field>\n </mat-card-content>\n </mat-card>\n </div>\n }\n\n @case ('preview') {\n <p class=\"editor-caption\">\n {{ t('praxis.charts.editor.preview.caption', 'Local preview derived from the canonical contract without remote calls.') }}\n </p>\n }\n }\n </mat-card-content>\n </mat-card>\n </div>\n\n <div class=\"editor-side\">\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.issues.title', 'Validation issues') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content>\n @if (issues().length) {\n <ul class=\"editor-issues\">\n @for (issue of issues(); track issueTrackBy($index, issue)) {\n <li class=\"editor-issue\">\n <strong>{{ issue.field }}</strong>\n <span>{{ issue.message }}</span>\n </li>\n }\n </ul>\n } @else {\n <div class=\"editor-empty\">\n {{ t('praxis.charts.editor.issues.empty', 'No issues were identified.') }}\n </div>\n }\n </mat-card-content>\n </mat-card>\n\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.preview.title', 'Chart preview') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content>\n @if (preview(); as chartPreview) {\n <p class=\"editor-caption\">\n {{ t('praxis.charts.editor.preview.caption', 'Local preview derived from the canonical contract without remote calls.') }}\n </p>\n <praxis-chart [config]=\"chartPreview.config\" [data]=\"chartPreview.data\"></praxis-chart>\n } @else {\n <div class=\"editor-empty\">\n {{ t('praxis.charts.editor.preview.invalid', 'Preview is unavailable while the contract has blocking errors.') }}\n </div>\n }\n </mat-card-content>\n </mat-card>\n </div>\n </div>\n</div>\n", styles: [":host{display:block;min-width:0;color:var(--md-sys-color-on-surface, #1a1b20)}.editor-shell{display:grid;gap:18px}.editor-nav{display:flex;gap:8px;flex-wrap:wrap}.editor-nav button.active{background:color-mix(in srgb,var(--md-sys-color-primary, #1263b4) 18%,transparent);color:var(--md-sys-color-primary, #1263b4)}.editor-layout{display:grid;gap:18px;grid-template-columns:minmax(0,1.35fr) minmax(320px,.9fr);align-items:start}.editor-form,.editor-side{display:grid;gap:16px}.editor-card{border-radius:20px;border:1px solid color-mix(in srgb,var(--md-sys-color-outline, #c5c7ce) 72%,transparent);background:linear-gradient(180deg,#1263b408,#1263b400)}.editor-grid{display:grid;gap:14px;grid-template-columns:repeat(auto-fit,minmax(220px,1fr))}.editor-stack{display:grid;gap:14px}.editor-row-card{padding:14px;border-radius:16px;border:1px solid color-mix(in srgb,var(--md-sys-color-outline, #c5c7ce) 54%,transparent);background:color-mix(in srgb,var(--md-sys-color-surface, #fff) 92%,rgba(18,99,180,.04))}.editor-row-actions{display:flex;justify-content:flex-end}.editor-field{width:100%}.editor-issues{display:grid;gap:10px;margin:0;padding:0;list-style:none}.editor-issue{padding:12px 14px;border-radius:14px;background:color-mix(in srgb,var(--md-sys-color-error, #b3261e) 8%,transparent);border:1px solid color-mix(in srgb,var(--md-sys-color-error, #b3261e) 18%,transparent)}.editor-issue strong{display:block;margin-bottom:4px}.editor-caption{margin:0 0 12px;color:var(--md-sys-color-on-surface-variant, #5a5d67);font-size:.92rem}.editor-empty{padding:18px;border-radius:14px;background:color-mix(in srgb,var(--md-sys-color-surface-variant, #eceff4) 78%,transparent)}@media(max-width:960px){.editor-layout{grid-template-columns:minmax(0,1fr)}}\n"] }]
|
|
6852
|
+
], providers: [providePraxisChartsI18n()], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"editor-shell\">\n <div class=\"editor-nav\">\n @for (section of sections; track section.id) {\n <button\n mat-stroked-button\n type=\"button\"\n [attr.data-testid]=\"'chart-editor-section-' + section.id\"\n [class.active]=\"activeSection() === section.id\"\n (click)=\"setSection(section.id)\"\n >\n {{ t(section.labelKey, section.fallback) }}\n </button>\n }\n </div>\n\n <div class=\"editor-layout\">\n <div class=\"editor-form\">\n <mat-card class=\"editor-card\">\n <mat-card-content>\n @switch (activeSection()) {\n @case ('general') {\n <div class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.chartId', 'Chart ID') }}</mat-label>\n <input matInput [ngModel]=\"doc().chartId || ''\" (ngModelChange)=\"setChartId($event)\" [disabled]=\"isReadonly()\" />\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.kind', 'Kind') }}</mat-label>\n <mat-select [ngModel]=\"doc().kind\" (ngModelChange)=\"setKind($event)\" [disabled]=\"isReadonly()\">\n @for (kind of chartKinds; track kind) {\n <mat-option [value]=\"kind\">\n {{ t('praxis.charts.editor.kind.' + kind, kind) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.title', 'Title') }}</mat-label>\n <input matInput data-testid=\"chart-editor-title-input\" [ngModel]=\"titleValue()\" (ngModelChange)=\"setTitle($event)\" [disabled]=\"isReadonly()\" />\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.subtitle', 'Subtitle') }}</mat-label>\n <input matInput [ngModel]=\"subtitleValue()\" (ngModelChange)=\"setSubtitle($event)\" [disabled]=\"isReadonly()\" />\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.sizingMode', 'Sizing') }}</mat-label>\n <mat-select [ngModel]=\"sizingModeValue()\" (ngModelChange)=\"setSizingMode($event)\" [disabled]=\"isReadonly()\">\n @for (mode of sizingModes; track mode) {\n <mat-option [value]=\"mode\">\n {{ t('praxis.charts.editor.sizing.' + mode, mode) }}\n </mat-option>\n }\n </mat-select>\n <mat-hint>{{ t('praxis.charts.editor.hint.sizingMode', 'Use fill-container only when the host widget provides a defined body height.') }}</mat-hint>\n </mat-form-field>\n\n @if (sizingModeValue() === 'fixed') {\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.height', 'Height') }}</mat-label>\n <input matInput [ngModel]=\"heightValue()\" (ngModelChange)=\"setSizingHeight($event)\" [disabled]=\"isReadonly()\" />\n <mat-hint>{{ t('praxis.charts.editor.hint.height', 'Numbers are saved as pixels; CSS lengths such as 20rem are also accepted.') }}</mat-hint>\n </mat-form-field>\n }\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.minHeight', 'Minimum height') }}</mat-label>\n <input matInput [ngModel]=\"sizingMinHeightValue()\" (ngModelChange)=\"setSizingMinHeight($event)\" [disabled]=\"isReadonly()\" />\n <mat-hint>{{ t('praxis.charts.editor.hint.minHeight', 'Set a readable minimum for compact dashboard widgets.') }}</mat-hint>\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.maxHeight', 'Maximum height') }}</mat-label>\n <input matInput [ngModel]=\"sizingMaxHeightValue()\" (ngModelChange)=\"setSizingMaxHeight($event)\" [disabled]=\"isReadonly()\" />\n <mat-hint>{{ t('praxis.charts.editor.hint.maxHeight', 'Leave empty unless the chart must stop growing inside a flexible layout.') }}</mat-hint>\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.aspectRatio', 'Aspect ratio') }}</mat-label>\n <input matInput [ngModel]=\"sizingAspectRatioValue()\" (ngModelChange)=\"setSizingAspectRatio($event)\" [disabled]=\"isReadonly()\" />\n <mat-hint>{{ t('praxis.charts.editor.hint.aspectRatio', 'Optional. Use values such as 1.777 or 16 / 9.') }}</mat-hint>\n </mat-form-field>\n </div>\n }\n\n @case ('data') {\n <div class=\"editor-stack\">\n <div class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.sourceKind', 'Source') }}</mat-label>\n <mat-select data-testid=\"chart-editor-source-kind\" [ngModel]=\"doc().source.kind\" (ngModelChange)=\"setSourceKind($event)\" [disabled]=\"isReadonly()\">\n @for (sourceKind of sourceKinds; track sourceKind) {\n <mat-option [value]=\"sourceKind\">\n {{ t('praxis.charts.editor.sourceKind.' + (sourceKind === 'praxis.stats' ? 'praxisStats' : 'derived'), sourceKind) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n @if (doc().source.kind === 'praxis.stats') {\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.resource', 'Resource') }}</mat-label>\n <mat-select data-testid=\"chart-editor-resource\" [ngModel]=\"resourceValue()\" (ngModelChange)=\"setResource($event)\" [disabled]=\"isReadonly() || resourceCatalogUnavailable()\">\n @for (resource of resourceOptions(); track resource.id) {\n <mat-option [value]=\"resource.path\">{{ resource.label }}</mat-option>\n }\n </mat-select>\n @if (resourceCatalogUnavailable()) {\n <mat-hint>{{ t('praxis.charts.editor.catalog.resourceMissing', 'Resource catalog is required for governed praxis.stats authoring.') }}</mat-hint>\n }\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.operation', 'Operation') }}</mat-label>\n <mat-select\n data-testid=\"chart-editor-operation\"\n [ngModel]=\"doc().source.operation || 'group-by'\"\n (ngModelChange)=\"setOperation($event)\"\n [disabled]=\"isReadonly() || operationCatalogUnavailable()\"\n >\n @for (operation of operationOptions(); track operation) {\n <mat-option [value]=\"operation\">\n {{ t('praxis.charts.editor.operation.' + (operation === 'group-by' ? 'groupBy' : operation), operation) }}\n </mat-option>\n }\n </mat-select>\n @if (operationCatalogUnavailable()) {\n <mat-hint>{{ t('praxis.charts.editor.catalog.operationMissing', 'Selected resource does not publish authorable stats operations.') }}</mat-hint>\n }\n </mat-form-field>\n }\n </div>\n\n @if (showTimeseriesControls()) {\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.specialization.timeseriesTitle', 'Timeseries options') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.granularity', 'Granularity') }}</mat-label>\n <mat-select [ngModel]=\"granularityValue()\" (ngModelChange)=\"setGranularity($event)\" [disabled]=\"isReadonly()\">\n @for (granularity of timeGranularities; track granularity) {\n <mat-option [value]=\"granularity\">\n {{ t('praxis.charts.editor.granularity.' + granularity, granularity) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n <mat-slide-toggle\n [ngModel]=\"fillGapsValue()\"\n (ngModelChange)=\"setFillGaps($event)\"\n [disabled]=\"isReadonly()\"\n >\n {{ t('praxis.charts.editor.field.fillGaps', 'Fill missing intervals') }}\n </mat-slide-toggle>\n </mat-card-content>\n </mat-card>\n }\n\n @if (showDistributionControls()) {\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.specialization.distributionTitle', 'Distribution options') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.distributionMode', 'Distribution mode') }}</mat-label>\n <mat-select data-testid=\"chart-editor-distribution-mode\" [ngModel]=\"distributionModeValue()\" (ngModelChange)=\"setDistributionMode($event)\" [disabled]=\"isReadonly()\">\n @for (mode of distributionModes; track mode) {\n <mat-option [value]=\"mode\">\n {{ t('praxis.charts.editor.distributionMode.' + mode, mode) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n @if (distributionModeValue() === 'histogram') {\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.bucketSize', 'Bucket size') }}</mat-label>\n <input matInput data-testid=\"chart-editor-bucket-size\" [ngModel]=\"bucketSizeValue()\" (ngModelChange)=\"setBucketSize($event)\" [disabled]=\"isReadonly()\" />\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.bucketCount', 'Bucket count') }}</mat-label>\n <input matInput data-testid=\"chart-editor-bucket-count\" [ngModel]=\"bucketCountValue()\" (ngModelChange)=\"setBucketCount($event)\" [disabled]=\"isReadonly()\" />\n </mat-form-field>\n }\n </mat-card-content>\n </mat-card>\n }\n </div>\n }\n\n @case ('motion') {\n <div class=\"editor-grid\">\n <mat-slide-toggle\n [ngModel]=\"normalizedDocument().motion?.enabled !== false\"\n (ngModelChange)=\"setMotionEnabled($event)\"\n [disabled]=\"isReadonly()\"\n >\n {{ t('praxis.charts.editor.field.motionEnabled', 'Enable animations') }}\n </mat-slide-toggle>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.motionPreset', 'Motion preset') }}</mat-label>\n <mat-select\n [ngModel]=\"normalizedDocument().motion?.preset || 'standard'\"\n (ngModelChange)=\"setMotionPreset($event)\"\n [disabled]=\"isReadonly() || normalizedDocument().motion?.enabled === false\"\n >\n @for (preset of motionPresets; track preset) {\n <mat-option [value]=\"preset\">\n {{ t('praxis.charts.editor.motionPreset.' + preset, preset) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n </div>\n }\n\n @case ('appearance') {\n <div class=\"editor-stack\">\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.appearance.featuresTitle', 'Display features') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-grid\">\n <mat-slide-toggle\n [ngModel]=\"featureEnabled('legend')\"\n (ngModelChange)=\"setFeatureEnabled('legend', $event)\"\n [disabled]=\"isReadonly()\"\n >\n {{ t('praxis.charts.editor.field.legendEnabled', 'Show legend') }}\n </mat-slide-toggle>\n\n <mat-slide-toggle\n [ngModel]=\"featureEnabled('labels')\"\n (ngModelChange)=\"setFeatureEnabled('labels', $event)\"\n [disabled]=\"isReadonly()\"\n >\n {{ t('praxis.charts.editor.field.labelsEnabled', 'Show labels') }}\n </mat-slide-toggle>\n\n <mat-slide-toggle\n [ngModel]=\"featureEnabled('tooltip')\"\n (ngModelChange)=\"setFeatureEnabled('tooltip', $event)\"\n [disabled]=\"isReadonly()\"\n >\n {{ t('praxis.charts.editor.field.tooltipEnabled', 'Show tooltip') }}\n </mat-slide-toggle>\n </mat-card-content>\n </mat-card>\n\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.appearance.paletteTitle', 'Palette') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-stack\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.themeVariant', 'Theme variant') }}</mat-label>\n <mat-select [ngModel]=\"themeVariantValue()\" (ngModelChange)=\"setThemeVariant($event)\" [disabled]=\"isReadonly()\">\n <mat-option [value]=\"''\">\n {{ t('praxis.charts.editor.themeVariant.none', 'No variant') }}\n </mat-option>\n @for (variant of themeVariants; track variant) {\n <mat-option [value]=\"variant\">\n {{ t('praxis.charts.editor.themeVariant.' + variant, variant) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.paletteMode', 'Palette mode') }}</mat-label>\n <mat-select [ngModel]=\"paletteModeValue()\" (ngModelChange)=\"setPaletteMode($event)\" [disabled]=\"isReadonly()\">\n @for (mode of paletteModes; track mode) {\n <mat-option [value]=\"mode\">\n {{ t('praxis.charts.editor.paletteMode.' + mode, mode) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n @if (paletteModeValue() === 'token') {\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.paletteToken', 'Palette token') }}</mat-label>\n <mat-select [ngModel]=\"paletteTokenValue()\" (ngModelChange)=\"setPaletteToken($event)\" [disabled]=\"isReadonly()\">\n @for (token of paletteTokens; track token) {\n <mat-option [value]=\"token\">\n {{ t('praxis.charts.editor.paletteToken.' + token, token) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n } @else {\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.palette', 'Palette colors') }}</mat-label>\n <textarea\n matInput\n rows=\"3\"\n [ngModel]=\"paletteValue()\"\n (ngModelChange)=\"setPalette($event)\"\n [disabled]=\"isReadonly()\"\n ></textarea>\n </mat-form-field>\n }\n\n <p class=\"editor-caption\">\n {{ t('praxis.charts.editor.appearance.paletteHint', 'Use a registered token or comma-separated colors to persist theme.palette in the canonical contract.') }}\n </p>\n </mat-card-content>\n </mat-card>\n\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.appearance.surfaceTitle', 'Surface') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content>\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.surfaceMode', 'Surface mode') }}</mat-label>\n <mat-select [ngModel]=\"surfaceModeValue()\" (ngModelChange)=\"setSurfaceMode($event)\" [disabled]=\"isReadonly()\">\n @for (mode of surfaceModes; track mode) {\n <mat-option [value]=\"mode\">\n {{ t('praxis.charts.editor.surface.' + mode, mode) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n <p class=\"editor-caption\">\n {{ t('praxis.charts.editor.appearance.surfaceHint', 'Use embedded for dashboard widgets and contained only when the chart must own its visual surface.') }}\n </p>\n </mat-card-content>\n </mat-card>\n\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.appearance.statesTitle', 'State messages') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-stack\">\n <div class=\"editor-row-card\">\n <div class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.emptyTitle', 'Empty title') }}</mat-label>\n <input matInput [ngModel]=\"stateTitle('empty')\" (ngModelChange)=\"setStateTitle('empty', $event)\" [disabled]=\"isReadonly()\" />\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.emptyDescription', 'Empty description') }}</mat-label>\n <textarea matInput rows=\"2\" [ngModel]=\"stateDescription('empty')\" (ngModelChange)=\"setStateDescription('empty', $event)\" [disabled]=\"isReadonly()\"></textarea>\n </mat-form-field>\n </div>\n </div>\n\n <div class=\"editor-row-card\">\n <div class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.loadingTitle', 'Loading title') }}</mat-label>\n <input matInput [ngModel]=\"stateTitle('loading')\" (ngModelChange)=\"setStateTitle('loading', $event)\" [disabled]=\"isReadonly()\" />\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.loadingDescription', 'Loading description') }}</mat-label>\n <textarea matInput rows=\"2\" [ngModel]=\"stateDescription('loading')\" (ngModelChange)=\"setStateDescription('loading', $event)\" [disabled]=\"isReadonly()\"></textarea>\n </mat-form-field>\n </div>\n </div>\n\n <div class=\"editor-row-card\">\n <div class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.errorTitle', 'Error title') }}</mat-label>\n <input matInput [ngModel]=\"stateTitle('error')\" (ngModelChange)=\"setStateTitle('error', $event)\" [disabled]=\"isReadonly()\" />\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.errorDescription', 'Error description') }}</mat-label>\n <textarea matInput rows=\"2\" [ngModel]=\"stateDescription('error')\" (ngModelChange)=\"setStateDescription('error', $event)\" [disabled]=\"isReadonly()\"></textarea>\n </mat-form-field>\n </div>\n </div>\n </mat-card-content>\n </mat-card>\n </div>\n }\n\n @case ('analytics') {\n <div class=\"editor-stack\">\n @if (showComboPanel()) {\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.specialization.comboTitle', 'Combo guidance') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content>\n <p class=\"editor-caption\">\n {{ t('praxis.charts.editor.specialization.comboHint', 'Combo charts require at least two metrics and allow per-metric axis and series kind mapping.') }}\n </p>\n </mat-card-content>\n </mat-card>\n }\n\n @if (showPieDonutPanel()) {\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.specialization.pieDonutTitle', 'Composition guidance') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content>\n <p class=\"editor-caption\">\n {{ t('praxis.charts.editor.specialization.pieDonutHint', 'Pie and donut charts keep only the first metric and use the first dimension as the category segment.') }}\n </p>\n </mat-card-content>\n </mat-card>\n }\n\n @if (showScatterPanel()) {\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.specialization.scatterTitle', 'Scatter guidance') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content>\n <p class=\"editor-caption\">\n {{ t('praxis.charts.editor.specialization.scatterHint', 'Scatter charts use the first dimension as X and the first metric as Y, so keep both fields mapped.') }}\n </p>\n </mat-card-content>\n </mat-card>\n }\n\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.analytics.dimensionsTitle', 'Dimensions') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-stack\">\n @for (dimension of dimensions(); track $index) {\n <div class=\"editor-row-card\" [attr.data-testid]=\"'chart-editor-dimension-row-' + $index\">\n <div class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\" [attr.data-testid]=\"'chart-editor-dimension-field-' + $index\">\n <mat-label>{{ t('praxis.charts.editor.field.dimension', 'Dimension') }}</mat-label>\n @if (fieldOptions('dimension').length) {\n <mat-select [ngModel]=\"dimension.field || ''\" (ngModelChange)=\"setDimensionField($index, $event)\" [disabled]=\"isReadonly()\">\n @for (field of fieldOptions('dimension'); track field.field) {\n <mat-option [value]=\"field.field\">{{ fieldOptionLabel(field) }}</mat-option>\n }\n </mat-select>\n } @else {\n <mat-select [ngModel]=\"dimension.field || ''\" (ngModelChange)=\"setDimensionField($index, $event)\" [disabled]=\"true\"></mat-select>\n <mat-hint>{{ t('praxis.charts.editor.catalog.dimensionFieldsMissing', 'No governed dimensions are available for the current resource and operation.') }}</mat-hint>\n }\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\" [attr.data-testid]=\"'chart-editor-dimension-role-' + $index\">\n <mat-label>{{ t('praxis.charts.editor.field.dimensionRole', 'Dimension role') }}</mat-label>\n <mat-select [ngModel]=\"dimension.role || 'category'\" (ngModelChange)=\"setDimensionRole($index, $event)\" [disabled]=\"isReadonly()\">\n @for (role of dimensionRoles; track role) {\n <mat-option [value]=\"role\">\n {{ t('praxis.charts.editor.dimensionRole.' + role, role) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n </div>\n\n <div class=\"editor-row-actions\">\n <button mat-button type=\"button\" (click)=\"removeDimension($index)\" [disabled]=\"isReadonly() || dimensions().length <= 1\">\n {{ t('praxis.charts.editor.analytics.removeDimension', 'Remove dimension') }}\n </button>\n </div>\n </div>\n }\n\n <div>\n <button mat-stroked-button type=\"button\" data-testid=\"chart-editor-add-dimension\" (click)=\"addDimension()\" [disabled]=\"isReadonly()\">\n {{ t('praxis.charts.editor.analytics.addDimension', 'Add dimension') }}\n </button>\n </div>\n </mat-card-content>\n </mat-card>\n\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.analytics.metricsTitle', 'Metrics') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-stack\">\n @for (metric of metrics(); track $index) {\n <div class=\"editor-row-card\" [attr.data-testid]=\"'chart-editor-metric-row-' + $index\">\n <div class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\" [attr.data-testid]=\"'chart-editor-metric-field-' + $index\">\n <mat-label>{{ t('praxis.charts.editor.field.metric', 'Metric') }}</mat-label>\n @if (fieldOptions('metric').length) {\n <mat-select [ngModel]=\"metric.field || ''\" (ngModelChange)=\"setMetricField($index, $event)\" [disabled]=\"isReadonly()\">\n @for (field of fieldOptions('metric'); track field.field) {\n <mat-option [value]=\"field.field\">{{ fieldOptionLabel(field) }}</mat-option>\n }\n </mat-select>\n } @else {\n <mat-select [ngModel]=\"metric.field || ''\" (ngModelChange)=\"setMetricField($index, $event)\" [disabled]=\"true\"></mat-select>\n <mat-hint>{{ t('praxis.charts.editor.catalog.metricFieldsMissing', 'No governed metrics are available for the current resource and operation.') }}</mat-hint>\n }\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\" [attr.data-testid]=\"'chart-editor-metric-label-' + $index\">\n <mat-label>{{ t('praxis.charts.editor.field.metricLabel', 'Metric label') }}</mat-label>\n <input matInput [ngModel]=\"metric.label || ''\" (ngModelChange)=\"setMetricLabel($index, $event)\" [disabled]=\"isReadonly()\" />\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\" [attr.data-testid]=\"'chart-editor-metric-aggregation-' + $index\">\n <mat-label>{{ t('praxis.charts.editor.field.metricAggregation', 'Aggregation') }}</mat-label>\n <mat-select [ngModel]=\"metric.aggregation || 'sum'\" (ngModelChange)=\"setMetricAggregation($index, $event)\" [disabled]=\"isReadonly()\">\n @for (aggregation of metricAggregationOptions(metric.field); track aggregation) {\n <mat-option [value]=\"aggregation\">\n {{ t('praxis.charts.editor.metricAggregation.' + aggregation, aggregation) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n @if (showMetricAxisControls()) {\n <mat-form-field class=\"editor-field\" appearance=\"outline\" [attr.data-testid]=\"'chart-editor-metric-axis-' + $index\">\n <mat-label>{{ t('praxis.charts.editor.field.metricAxis', 'Axis') }}</mat-label>\n <mat-select [ngModel]=\"metric.axis || 'primary'\" (ngModelChange)=\"setMetricAxis($index, $event)\" [disabled]=\"isReadonly()\">\n @for (axis of metricAxes; track axis) {\n <mat-option [value]=\"axis\">\n {{ t('praxis.charts.editor.metricAxis.' + axis, axis) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n }\n\n @if (showMetricSeriesKindControls()) {\n <mat-form-field class=\"editor-field\" appearance=\"outline\" [attr.data-testid]=\"'chart-editor-metric-series-kind-' + $index\">\n <mat-label>{{ t('praxis.charts.editor.field.metricSeriesKind', 'Series kind') }}</mat-label>\n <mat-select [ngModel]=\"metric.seriesKind || 'bar'\" (ngModelChange)=\"setMetricSeriesKind($index, $event)\" [disabled]=\"isReadonly()\">\n @for (seriesKind of metricSeriesKinds; track seriesKind) {\n <mat-option [value]=\"seriesKind\">\n {{ t('praxis.charts.editor.metricSeriesKind.' + seriesKind, seriesKind) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n }\n </div>\n\n <div class=\"editor-row-actions\">\n <button mat-button type=\"button\" [attr.data-testid]=\"'chart-editor-remove-metric-' + $index\" (click)=\"removeMetric($index)\" [disabled]=\"isReadonly() || metrics().length <= 1\">\n {{ t('praxis.charts.editor.analytics.removeMetric', 'Remove metric') }}\n </button>\n </div>\n </div>\n }\n\n <div>\n <button mat-stroked-button type=\"button\" data-testid=\"chart-editor-add-metric\" (click)=\"addMetric()\" [disabled]=\"isReadonly()\">\n {{ t('praxis.charts.editor.analytics.addMetric', 'Add metric') }}\n </button>\n </div>\n </mat-card-content>\n </mat-card>\n </div>\n }\n\n @case ('events') {\n <div class=\"editor-stack\">\n <mat-card class=\"editor-card\" data-testid=\"chart-editor-event-pointClick\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.events.pointClickTitle', 'Point click') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventAction', 'Action') }}</mat-label>\n <mat-select data-testid=\"chart-editor-event-pointClick-action\" [ngModel]=\"eventAction('pointClick')\" (ngModelChange)=\"setEventAction('pointClick', $event)\" [disabled]=\"isReadonly()\">\n <mat-option value=\"\">{{ t('praxis.charts.editor.events.none', 'None') }}</mat-option>\n @for (action of eventActionOptions; track action) {\n <mat-option [value]=\"action\">\n {{ t('praxis.charts.editor.eventAction.' + action, action) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventTarget', 'Target') }}</mat-label>\n <mat-select data-testid=\"chart-editor-event-pointClick-target\" [ngModel]=\"eventTarget('pointClick')\" (ngModelChange)=\"setEventTarget('pointClick', $event)\" [disabled]=\"isReadonly() || !eventAction('pointClick') || targetCatalogUnavailable(eventAction('pointClick'), 'pointClick')\">\n @for (target of targetOptions(eventAction('pointClick'), 'pointClick'); track target.id) {\n <mat-option [value]=\"target.id\">{{ target.label }}</mat-option>\n }\n </mat-select>\n @if (targetCatalogUnavailable(eventAction('pointClick'), 'pointClick')) {\n <mat-hint>{{ t('praxis.charts.editor.catalog.targetMissing', 'Target catalog is required for this governed event action.') }}</mat-hint>\n }\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventMapping', 'Mapping') }}</mat-label>\n <textarea\n matInput\n data-testid=\"chart-editor-event-pointClick-mapping\"\n rows=\"4\"\n [ngModel]=\"eventMappingText('pointClick')\"\n (ngModelChange)=\"setEventMapping('pointClick', $event)\"\n [disabled]=\"isReadonly() || !eventAction('pointClick')\"\n ></textarea>\n </mat-form-field>\n </mat-card-content>\n </mat-card>\n\n <mat-card class=\"editor-card\" data-testid=\"chart-editor-event-selectionChange\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.events.selectionChangeTitle', 'Selection change') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventAction', 'Action') }}</mat-label>\n <mat-select data-testid=\"chart-editor-event-selectionChange-action\" [ngModel]=\"eventAction('selectionChange')\" (ngModelChange)=\"setEventAction('selectionChange', $event)\" [disabled]=\"isReadonly()\">\n <mat-option value=\"\">{{ t('praxis.charts.editor.events.none', 'None') }}</mat-option>\n @for (action of eventActionOptions; track action) {\n <mat-option [value]=\"action\">\n {{ t('praxis.charts.editor.eventAction.' + action, action) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventTarget', 'Target') }}</mat-label>\n <mat-select data-testid=\"chart-editor-event-selectionChange-target\" [ngModel]=\"eventTarget('selectionChange')\" (ngModelChange)=\"setEventTarget('selectionChange', $event)\" [disabled]=\"isReadonly() || !eventAction('selectionChange') || targetCatalogUnavailable(eventAction('selectionChange'), 'selectionChange')\">\n @for (target of targetOptions(eventAction('selectionChange'), 'selectionChange'); track target.id) {\n <mat-option [value]=\"target.id\">{{ target.label }}</mat-option>\n }\n </mat-select>\n @if (targetCatalogUnavailable(eventAction('selectionChange'), 'selectionChange')) {\n <mat-hint>{{ t('praxis.charts.editor.catalog.targetMissing', 'Target catalog is required for this governed event action.') }}</mat-hint>\n }\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventMapping', 'Mapping') }}</mat-label>\n <textarea\n matInput\n data-testid=\"chart-editor-event-selectionChange-mapping\"\n rows=\"4\"\n [ngModel]=\"eventMappingText('selectionChange')\"\n (ngModelChange)=\"setEventMapping('selectionChange', $event)\"\n [disabled]=\"isReadonly() || !eventAction('selectionChange')\"\n ></textarea>\n </mat-form-field>\n </mat-card-content>\n </mat-card>\n\n <mat-card class=\"editor-card\" data-testid=\"chart-editor-event-drillDown\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.events.drillDownTitle', 'Drill down') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventAction', 'Action') }}</mat-label>\n <mat-select data-testid=\"chart-editor-event-drillDown-action\" [ngModel]=\"eventAction('drillDown')\" (ngModelChange)=\"setEventAction('drillDown', $event)\" [disabled]=\"isReadonly()\">\n <mat-option value=\"\">{{ t('praxis.charts.editor.events.none', 'None') }}</mat-option>\n @for (action of eventActionOptions; track action) {\n <mat-option [value]=\"action\">\n {{ t('praxis.charts.editor.eventAction.' + action, action) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventTarget', 'Target') }}</mat-label>\n <mat-select data-testid=\"chart-editor-event-drillDown-target\" [ngModel]=\"eventTarget('drillDown')\" (ngModelChange)=\"setEventTarget('drillDown', $event)\" [disabled]=\"isReadonly() || !eventAction('drillDown') || targetCatalogUnavailable(eventAction('drillDown'), 'drillDown')\">\n @for (target of targetOptions(eventAction('drillDown'), 'drillDown'); track target.id) {\n <mat-option [value]=\"target.id\">{{ target.label }}</mat-option>\n }\n </mat-select>\n @if (targetCatalogUnavailable(eventAction('drillDown'), 'drillDown')) {\n <mat-hint>{{ t('praxis.charts.editor.catalog.targetMissing', 'Target catalog is required for this governed event action.') }}</mat-hint>\n }\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventMapping', 'Mapping') }}</mat-label>\n <textarea\n matInput\n data-testid=\"chart-editor-event-drillDown-mapping\"\n rows=\"4\"\n [ngModel]=\"eventMappingText('drillDown')\"\n (ngModelChange)=\"setEventMapping('drillDown', $event)\"\n [disabled]=\"isReadonly() || !eventAction('drillDown')\"\n ></textarea>\n </mat-form-field>\n </mat-card-content>\n </mat-card>\n\n <mat-card class=\"editor-card\" data-testid=\"chart-editor-event-crossFilter\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.events.crossFilterTitle', 'Cross filter') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventAction', 'Action') }}</mat-label>\n <mat-select data-testid=\"chart-editor-event-crossFilter-action\" [ngModel]=\"eventAction('crossFilter')\" (ngModelChange)=\"setEventAction('crossFilter', $event)\" [disabled]=\"isReadonly()\">\n <mat-option value=\"\">{{ t('praxis.charts.editor.events.none', 'None') }}</mat-option>\n @for (action of eventActionOptions; track action) {\n <mat-option [value]=\"action\">\n {{ t('praxis.charts.editor.eventAction.' + action, action) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventTarget', 'Target') }}</mat-label>\n <mat-select data-testid=\"chart-editor-event-crossFilter-target\" [ngModel]=\"eventTarget('crossFilter')\" (ngModelChange)=\"setEventTarget('crossFilter', $event)\" [disabled]=\"isReadonly() || !eventAction('crossFilter') || targetCatalogUnavailable(eventAction('crossFilter'), 'crossFilter')\">\n @for (target of targetOptions(eventAction('crossFilter'), 'crossFilter'); track target.id) {\n <mat-option [value]=\"target.id\">{{ target.label }}</mat-option>\n }\n </mat-select>\n @if (targetCatalogUnavailable(eventAction('crossFilter'), 'crossFilter')) {\n <mat-hint>{{ t('praxis.charts.editor.catalog.targetMissing', 'Target catalog is required for this governed event action.') }}</mat-hint>\n }\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventMapping', 'Mapping') }}</mat-label>\n <textarea\n matInput\n data-testid=\"chart-editor-event-crossFilter-mapping\"\n rows=\"4\"\n [ngModel]=\"eventMappingText('crossFilter')\"\n (ngModelChange)=\"setEventMapping('crossFilter', $event)\"\n [disabled]=\"isReadonly() || !eventAction('crossFilter')\"\n ></textarea>\n </mat-form-field>\n </mat-card-content>\n </mat-card>\n </div>\n }\n\n @case ('preview') {\n <p class=\"editor-caption\">\n {{ t('praxis.charts.editor.preview.caption', 'Local preview derived from the canonical contract without remote calls.') }}\n </p>\n }\n }\n </mat-card-content>\n </mat-card>\n </div>\n\n <div class=\"editor-side\">\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.issues.title', 'Validation issues') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content>\n @if (issues().length) {\n <ul class=\"editor-issues\">\n @for (issue of issues(); track issueTrackBy($index, issue)) {\n <li class=\"editor-issue\">\n <strong>{{ issue.field }}</strong>\n <span>{{ issue.message }}</span>\n </li>\n }\n </ul>\n } @else {\n <div class=\"editor-empty\">\n {{ t('praxis.charts.editor.issues.empty', 'No issues were identified.') }}\n </div>\n }\n </mat-card-content>\n </mat-card>\n\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.preview.title', 'Chart preview') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content>\n @if (preview(); as chartPreview) {\n <p class=\"editor-caption\">\n {{ t('praxis.charts.editor.preview.caption', 'Local preview derived from the canonical contract without remote calls.') }}\n </p>\n <praxis-chart [config]=\"chartPreview.config\" [data]=\"chartPreview.data\"></praxis-chart>\n } @else {\n <div class=\"editor-empty\">\n {{ t('praxis.charts.editor.preview.invalid', 'Preview is unavailable while the contract has blocking errors.') }}\n </div>\n }\n </mat-card-content>\n </mat-card>\n </div>\n </div>\n</div>\n", styles: [":host{display:block;min-width:0;container:praxis-chart-editor / inline-size;color:var(--md-sys-color-on-surface, #1a1b20)}.editor-shell{display:grid;gap:18px;min-width:0}.editor-nav{display:flex;gap:8px;flex-wrap:wrap}.editor-nav button.active{background:color-mix(in srgb,var(--md-sys-color-primary, #1263b4) 18%,transparent);color:var(--md-sys-color-primary, #1263b4)}.editor-layout{display:grid;gap:18px;grid-template-columns:minmax(0,1.35fr) minmax(320px,.9fr);align-items:start}.editor-form,.editor-side{display:grid;gap:16px;min-width:0}.editor-card{box-sizing:border-box;width:100%;min-width:0;border-radius:20px;border:1px solid color-mix(in srgb,var(--md-sys-color-outline, #c5c7ce) 72%,transparent);background:linear-gradient(180deg,#1263b408,#1263b400)}.editor-grid{display:grid;gap:14px;grid-template-columns:repeat(auto-fit,minmax(220px,1fr))}.editor-stack{display:grid;gap:14px}.editor-row-card{padding:14px;border-radius:16px;border:1px solid color-mix(in srgb,var(--md-sys-color-outline, #c5c7ce) 54%,transparent);background:color-mix(in srgb,var(--md-sys-color-surface, #fff) 92%,rgba(18,99,180,.04))}.editor-row-actions{display:flex;justify-content:flex-end}.editor-field{width:100%}.editor-issues{display:grid;gap:10px;margin:0;padding:0;list-style:none}.editor-issue{padding:12px 14px;border-radius:14px;background:color-mix(in srgb,var(--md-sys-color-error, #b3261e) 8%,transparent);border:1px solid color-mix(in srgb,var(--md-sys-color-error, #b3261e) 18%,transparent)}.editor-issue strong{display:block;margin-bottom:4px}.editor-caption{margin:0 0 12px;color:var(--md-sys-color-on-surface-variant, #5a5d67);font-size:.92rem}.editor-empty{padding:18px;border-radius:14px;background:color-mix(in srgb,var(--md-sys-color-surface-variant, #eceff4) 78%,transparent)}.editor-side praxis-chart{display:block;width:100%;min-width:0;max-width:100%}@container praxis-chart-editor (max-width: 840px){.editor-layout{grid-template-columns:minmax(0,1fr)}}@media(max-width:960px){.editor-layout{grid-template-columns:minmax(0,1fr)}}\n"] }]
|
|
5223
6853
|
}], ctorParameters: () => [], propDecorators: { documentInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "document", required: false }] }], modeInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "mode", required: false }] }], readonlyInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "readonly", required: false }] }], availableResourcesInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "availableResources", required: false }] }], availableFieldsInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "availableFields", required: false }] }], availableTargetsInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "availableTargets", required: false }] }], apply: [{ type: i0.Output, args: ["apply"] }], save: [{ type: i0.Output, args: ["save"] }], resetChange: [{ type: i0.Output, args: ["resetChange"] }], documentChange: [{ type: i0.Output, args: ["documentChange"] }] } });
|
|
5224
6854
|
|
|
5225
6855
|
var praxisChartConfigEditor = /*#__PURE__*/Object.freeze({
|
|
@@ -5228,66 +6858,118 @@ var praxisChartConfigEditor = /*#__PURE__*/Object.freeze({
|
|
|
5228
6858
|
});
|
|
5229
6859
|
|
|
5230
6860
|
class PraxisChartWidgetConfigEditor {
|
|
6861
|
+
i18n = inject(PraxisI18nService);
|
|
5231
6862
|
inputs = null;
|
|
5232
|
-
|
|
6863
|
+
context = null;
|
|
6864
|
+
contextDiagnostics = [];
|
|
6865
|
+
activeChartEditor;
|
|
6866
|
+
chartEditorSubscription = new Subscription();
|
|
6867
|
+
set chartEditor(value) {
|
|
6868
|
+
if (this.activeChartEditor === value) {
|
|
6869
|
+
return;
|
|
6870
|
+
}
|
|
6871
|
+
this.chartEditorSubscription.unsubscribe();
|
|
6872
|
+
this.chartEditorSubscription = new Subscription();
|
|
6873
|
+
this.activeChartEditor = value;
|
|
6874
|
+
this.chartEditorDirty = false;
|
|
6875
|
+
this.chartEditorValid = true;
|
|
6876
|
+
if (value) {
|
|
6877
|
+
this.bindChartEditor(value);
|
|
6878
|
+
}
|
|
6879
|
+
this.updateDirty();
|
|
6880
|
+
this.updateValidity();
|
|
6881
|
+
}
|
|
6882
|
+
get chartEditor() {
|
|
6883
|
+
return this.activeChartEditor;
|
|
6884
|
+
}
|
|
5233
6885
|
isDirty$ = new BehaviorSubject(false);
|
|
5234
6886
|
isValid$ = new BehaviorSubject(true);
|
|
5235
6887
|
isBusy$ = new BehaviorSubject(false);
|
|
5236
|
-
|
|
6888
|
+
chartEditorDirty = false;
|
|
5237
6889
|
chartEditorValid = true;
|
|
5238
6890
|
queryContextDirty = false;
|
|
6891
|
+
baselineQueryContextText = '';
|
|
5239
6892
|
queryContextText = '';
|
|
5240
6893
|
queryContextError = '';
|
|
5241
6894
|
get chartDocument() {
|
|
5242
6895
|
return this.inputs?.chartDocument ?? null;
|
|
5243
6896
|
}
|
|
5244
6897
|
get availableResources() {
|
|
5245
|
-
return this.inputs?.availableResources ?? [];
|
|
6898
|
+
return this.context?.availableResources ?? this.inputs?.availableResources ?? [];
|
|
5246
6899
|
}
|
|
5247
6900
|
get availableFields() {
|
|
5248
|
-
return this.inputs?.availableFields ?? [];
|
|
6901
|
+
return this.context?.availableFields ?? this.inputs?.availableFields ?? [];
|
|
5249
6902
|
}
|
|
5250
6903
|
get availableTargets() {
|
|
5251
|
-
return this.inputs?.availableTargets ?? [];
|
|
5252
|
-
}
|
|
5253
|
-
ngAfterViewInit() {
|
|
5254
|
-
if (!this.chartEditor) {
|
|
5255
|
-
return;
|
|
5256
|
-
}
|
|
5257
|
-
this.subscription.add(this.chartEditor.isDirty$.subscribe((value) => this.isDirty$.next(value || this.queryContextDirty)));
|
|
5258
|
-
this.subscription.add(this.chartEditor.isValid$.subscribe((value) => {
|
|
5259
|
-
this.chartEditorValid = value;
|
|
5260
|
-
this.updateValidity();
|
|
5261
|
-
}));
|
|
5262
|
-
this.subscription.add(this.chartEditor.isBusy$.subscribe((value) => this.isBusy$.next(value)));
|
|
6904
|
+
return this.context?.availableTargets ?? this.inputs?.availableTargets ?? [];
|
|
5263
6905
|
}
|
|
5264
6906
|
ngOnChanges(changes) {
|
|
5265
|
-
if (changes['inputs']) {
|
|
5266
|
-
this.
|
|
5267
|
-
this.queryContextError = '';
|
|
5268
|
-
this.queryContextDirty = false;
|
|
5269
|
-
this.isDirty$.next(this.chartEditor?.isDirty$.value ?? false);
|
|
5270
|
-
this.updateValidity();
|
|
6907
|
+
if (changes['inputs'] && (changes['inputs'].firstChange || !this.isDirty$.value)) {
|
|
6908
|
+
this.rebaseQueryContext();
|
|
5271
6909
|
}
|
|
5272
6910
|
}
|
|
5273
6911
|
ngOnDestroy() {
|
|
5274
|
-
this.
|
|
6912
|
+
this.chartEditorSubscription.unsubscribe();
|
|
5275
6913
|
}
|
|
5276
6914
|
setQueryContextText(value) {
|
|
5277
6915
|
this.queryContextText = value;
|
|
5278
|
-
this.queryContextDirty =
|
|
6916
|
+
this.queryContextDirty = value !== this.baselineQueryContextText;
|
|
5279
6917
|
this.queryContextError = this.validateQueryContextText(value);
|
|
5280
|
-
this.
|
|
6918
|
+
this.updateDirty();
|
|
5281
6919
|
this.updateValidity();
|
|
5282
6920
|
}
|
|
5283
6921
|
getSettingsValue() {
|
|
5284
|
-
return this.buildValue(this.
|
|
6922
|
+
return this.buildValue(this.getChartDocumentForApply());
|
|
5285
6923
|
}
|
|
5286
6924
|
onSave() {
|
|
5287
|
-
|
|
6925
|
+
const value = this.buildValue(this.isValid$.value
|
|
6926
|
+
? this.getChartDocumentForSave()
|
|
6927
|
+
: this.getChartDocumentForApply());
|
|
6928
|
+
if (this.isValid$.value) {
|
|
6929
|
+
this.baselineQueryContextText = this.queryContextText;
|
|
6930
|
+
this.queryContextDirty = false;
|
|
6931
|
+
this.updateDirty();
|
|
6932
|
+
}
|
|
6933
|
+
return value;
|
|
5288
6934
|
}
|
|
5289
6935
|
reset() {
|
|
5290
6936
|
this.chartEditor?.reset();
|
|
6937
|
+
this.queryContextText = this.baselineQueryContextText;
|
|
6938
|
+
this.queryContextError = '';
|
|
6939
|
+
this.queryContextDirty = false;
|
|
6940
|
+
this.updateDirty();
|
|
6941
|
+
this.updateValidity();
|
|
6942
|
+
}
|
|
6943
|
+
t(key, fallback) {
|
|
6944
|
+
return this.i18n.resolve(resolvePraxisChartsText({ key, text: fallback }, fallback));
|
|
6945
|
+
}
|
|
6946
|
+
contextDiagnosticText(diagnostic) {
|
|
6947
|
+
switch (diagnostic.code) {
|
|
6948
|
+
case 'chart-resource-catalog-denied':
|
|
6949
|
+
return this.t('praxis.charts.widget.contextDiagnostics.catalogDenied', 'Access to the governed resource catalog was denied.');
|
|
6950
|
+
case 'chart-resource-catalog-unavailable':
|
|
6951
|
+
return this.t('praxis.charts.widget.contextDiagnostics.catalogUnavailable', 'The governed resource catalog is temporarily unavailable.');
|
|
6952
|
+
case 'chart-resource-catalog-empty':
|
|
6953
|
+
return this.t('praxis.charts.widget.contextDiagnostics.catalogEmpty', 'No authorable stats resources were published by the governed catalog.');
|
|
6954
|
+
case 'chart-resource-catalog-path-invalid':
|
|
6955
|
+
return this.t('praxis.charts.widget.contextDiagnostics.catalogPathInvalid', 'The resource catalog published an unsafe or non-relative operational path.');
|
|
6956
|
+
case 'chart-resource-capabilities-denied':
|
|
6957
|
+
return this.t('praxis.charts.widget.contextDiagnostics.capabilitiesDenied', 'Access to the selected resource capabilities was denied.');
|
|
6958
|
+
case 'chart-resource-capabilities-unavailable':
|
|
6959
|
+
return this.t('praxis.charts.widget.contextDiagnostics.capabilitiesUnavailable', 'The selected resource capabilities are temporarily unavailable.');
|
|
6960
|
+
case 'chart-resource-stats-operations-empty':
|
|
6961
|
+
return this.t('praxis.charts.widget.contextDiagnostics.operationsEmpty', 'The selected resource does not publish authorable stats operations.');
|
|
6962
|
+
case 'chart-resource-stats-fields-empty':
|
|
6963
|
+
return this.t('praxis.charts.widget.contextDiagnostics.fieldsEmpty', 'The selected resource does not publish governed fields for chart authoring.');
|
|
6964
|
+
case 'chart-resource-capabilities-identity-mismatch':
|
|
6965
|
+
return this.t('praxis.charts.widget.contextDiagnostics.identityMismatch', 'The selected resource identity does not match its governed capability snapshot.');
|
|
6966
|
+
case 'chart-resource-capabilities-path-invalid':
|
|
6967
|
+
return this.t('praxis.charts.widget.contextDiagnostics.capabilitiesPathInvalid', 'The selected capability snapshot did not publish a safe governed resource path.');
|
|
6968
|
+
case 'chart-resource-capabilities-path-mismatch':
|
|
6969
|
+
return this.t('praxis.charts.widget.contextDiagnostics.capabilitiesPathMismatch', 'The selected capability snapshot path does not match the operational path published by the catalog.');
|
|
6970
|
+
default:
|
|
6971
|
+
return this.t('praxis.charts.widget.contextDiagnostics.partial', 'Some governed authoring choices could not be materialized from the current page context.');
|
|
6972
|
+
}
|
|
5291
6973
|
}
|
|
5292
6974
|
buildValue(document) {
|
|
5293
6975
|
const inputs = {
|
|
@@ -5295,19 +6977,57 @@ class PraxisChartWidgetConfigEditor {
|
|
|
5295
6977
|
...(document ? { chartDocument: document } : {}),
|
|
5296
6978
|
};
|
|
5297
6979
|
const queryContext = this.parseQueryContextText(this.queryContextText);
|
|
5298
|
-
if (queryContext === undefined) {
|
|
6980
|
+
if (queryContext.valid && queryContext.value === undefined) {
|
|
5299
6981
|
delete inputs.queryContext;
|
|
5300
6982
|
}
|
|
5301
|
-
else {
|
|
5302
|
-
inputs.queryContext = queryContext;
|
|
6983
|
+
else if (queryContext.valid) {
|
|
6984
|
+
inputs.queryContext = queryContext.value;
|
|
5303
6985
|
}
|
|
5304
6986
|
if (document?.source?.kind === 'praxis.stats') {
|
|
5305
6987
|
delete inputs.data;
|
|
5306
6988
|
}
|
|
6989
|
+
delete inputs.availableResources;
|
|
6990
|
+
delete inputs.availableFields;
|
|
6991
|
+
delete inputs.availableTargets;
|
|
6992
|
+
delete inputs.contextDiagnostics;
|
|
5307
6993
|
return {
|
|
5308
6994
|
inputs,
|
|
5309
6995
|
};
|
|
5310
6996
|
}
|
|
6997
|
+
bindChartEditor(editor) {
|
|
6998
|
+
this.chartEditorDirty = editor.isDirty$.value;
|
|
6999
|
+
this.chartEditorValid = editor.isValid$.value;
|
|
7000
|
+
this.isBusy$.next(editor.isBusy$.value);
|
|
7001
|
+
this.chartEditorSubscription.add(editor.isDirty$.subscribe((value) => {
|
|
7002
|
+
this.chartEditorDirty = value;
|
|
7003
|
+
this.updateDirty();
|
|
7004
|
+
}));
|
|
7005
|
+
this.chartEditorSubscription.add(editor.isValid$.subscribe((value) => {
|
|
7006
|
+
this.chartEditorValid = value;
|
|
7007
|
+
this.updateValidity();
|
|
7008
|
+
}));
|
|
7009
|
+
this.chartEditorSubscription.add(editor.isBusy$.subscribe((value) => this.isBusy$.next(value)));
|
|
7010
|
+
}
|
|
7011
|
+
getChartDocumentForApply() {
|
|
7012
|
+
return this.chartDocument ? this.chartEditor?.getSettingsValue() : undefined;
|
|
7013
|
+
}
|
|
7014
|
+
getChartDocumentForSave() {
|
|
7015
|
+
if (!this.chartDocument) {
|
|
7016
|
+
return undefined;
|
|
7017
|
+
}
|
|
7018
|
+
return this.chartEditor?.onSave?.() ?? this.chartEditor?.getSettingsValue();
|
|
7019
|
+
}
|
|
7020
|
+
rebaseQueryContext() {
|
|
7021
|
+
this.baselineQueryContextText = this.formatQueryContext(this.inputs?.queryContext);
|
|
7022
|
+
this.queryContextText = this.baselineQueryContextText;
|
|
7023
|
+
this.queryContextError = '';
|
|
7024
|
+
this.queryContextDirty = false;
|
|
7025
|
+
this.updateDirty();
|
|
7026
|
+
this.updateValidity();
|
|
7027
|
+
}
|
|
7028
|
+
updateDirty() {
|
|
7029
|
+
this.isDirty$.next(this.chartEditorDirty || this.queryContextDirty);
|
|
7030
|
+
}
|
|
5311
7031
|
updateValidity() {
|
|
5312
7032
|
this.isValid$.next(this.chartEditorValid && !this.queryContextError);
|
|
5313
7033
|
}
|
|
@@ -5326,42 +7046,70 @@ class PraxisChartWidgetConfigEditor {
|
|
|
5326
7046
|
const parsed = JSON.parse(trimmed);
|
|
5327
7047
|
return parsed && typeof parsed === 'object' && !Array.isArray(parsed)
|
|
5328
7048
|
? ''
|
|
5329
|
-
: 'Query context must be a JSON object.';
|
|
7049
|
+
: this.t('praxis.charts.widget.queryContext.mustBeObject', 'Query context must be a JSON object.');
|
|
5330
7050
|
}
|
|
5331
|
-
catch
|
|
5332
|
-
return
|
|
7051
|
+
catch {
|
|
7052
|
+
return this.t('praxis.charts.widget.queryContext.invalidJson', 'Query context must be valid JSON.');
|
|
5333
7053
|
}
|
|
5334
7054
|
}
|
|
5335
7055
|
parseQueryContextText(value) {
|
|
5336
7056
|
const trimmed = value.trim();
|
|
5337
7057
|
if (!trimmed) {
|
|
5338
|
-
return undefined;
|
|
7058
|
+
return { valid: true, value: undefined };
|
|
7059
|
+
}
|
|
7060
|
+
try {
|
|
7061
|
+
const parsed = JSON.parse(trimmed);
|
|
7062
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
7063
|
+
return { valid: false };
|
|
7064
|
+
}
|
|
7065
|
+
return { valid: true, value: parsed };
|
|
7066
|
+
}
|
|
7067
|
+
catch {
|
|
7068
|
+
return { valid: false };
|
|
5339
7069
|
}
|
|
5340
|
-
const parsed = JSON.parse(trimmed);
|
|
5341
|
-
return parsed && typeof parsed === 'object' && !Array.isArray(parsed)
|
|
5342
|
-
? parsed
|
|
5343
|
-
: null;
|
|
5344
7070
|
}
|
|
5345
7071
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: PraxisChartWidgetConfigEditor, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
5346
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.14", type: PraxisChartWidgetConfigEditor, isStandalone: true, selector: "praxis-chart-widget-config-editor", inputs: { inputs: "inputs" }, viewQueries: [{ propertyName: "chartEditor", first: true, predicate: ["chartEditor"], descendants: true }], usesOnChanges: true, ngImport: i0, template: `
|
|
7072
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.14", type: PraxisChartWidgetConfigEditor, isStandalone: true, selector: "praxis-chart-widget-config-editor", inputs: { inputs: "inputs", context: "context", contextDiagnostics: "contextDiagnostics" }, providers: [providePraxisChartsI18n()], viewQueries: [{ propertyName: "chartEditor", first: true, predicate: ["chartEditor"], descendants: true }], usesOnChanges: true, ngImport: i0, template: `
|
|
5347
7073
|
<section data-testid="chart-widget-config-editor">
|
|
5348
|
-
|
|
5349
|
-
|
|
5350
|
-
|
|
5351
|
-
|
|
5352
|
-
|
|
5353
|
-
|
|
5354
|
-
|
|
7074
|
+
@if (contextDiagnostics.length) {
|
|
7075
|
+
<section
|
|
7076
|
+
class="chart-widget-config-editor__diagnostics"
|
|
7077
|
+
data-testid="chart-widget-context-diagnostics"
|
|
7078
|
+
role="status"
|
|
7079
|
+
>
|
|
7080
|
+
<strong>{{ t('praxis.charts.widget.contextDiagnostics.title', 'Authoring context') }}</strong>
|
|
7081
|
+
<ul>
|
|
7082
|
+
@for (diagnostic of contextDiagnostics; track diagnostic.code + ':' + (diagnostic.path || '')) {
|
|
7083
|
+
<li [class]="'chart-widget-config-editor__diagnostic--' + (diagnostic.severity || 'info')">
|
|
7084
|
+
{{ contextDiagnosticText(diagnostic) }}
|
|
7085
|
+
</li>
|
|
7086
|
+
}
|
|
7087
|
+
</ul>
|
|
7088
|
+
</section>
|
|
7089
|
+
}
|
|
7090
|
+
@if (chartDocument) {
|
|
7091
|
+
<praxis-chart-config-editor
|
|
7092
|
+
#chartEditor
|
|
7093
|
+
[document]="chartDocument"
|
|
7094
|
+
[availableResources]="availableResources"
|
|
7095
|
+
[availableFields]="availableFields"
|
|
7096
|
+
[availableTargets]="availableTargets"
|
|
7097
|
+
/>
|
|
7098
|
+
} @else {
|
|
7099
|
+
<section class="chart-widget-config-editor__notice" data-testid="chart-widget-missing-document">
|
|
7100
|
+
{{ t('praxis.charts.widget.missingDocument', 'This widget does not have a canonical chart document yet. Runtime inputs are preserved until a canonical chartDocument is provided by the host.') }}
|
|
7101
|
+
</section>
|
|
7102
|
+
}
|
|
5355
7103
|
<section class="chart-widget-config-editor__runtime">
|
|
5356
7104
|
<label class="chart-widget-config-editor__label" for="chart-query-context">
|
|
5357
|
-
Query context
|
|
7105
|
+
{{ t('praxis.charts.widget.queryContext.label', 'Query context') }}
|
|
5358
7106
|
</label>
|
|
5359
7107
|
<textarea
|
|
5360
7108
|
id="chart-query-context"
|
|
5361
7109
|
class="chart-widget-config-editor__textarea"
|
|
5362
7110
|
rows="5"
|
|
5363
7111
|
spellcheck="false"
|
|
5364
|
-
aria-label="
|
|
7112
|
+
[attr.aria-label]="t('praxis.charts.widget.queryContext.ariaLabel', 'Chart query context')"
|
|
5365
7113
|
data-testid="chart-widget-query-context"
|
|
5366
7114
|
[ngModel]="queryContextText"
|
|
5367
7115
|
(ngModelChange)="setQueryContextText($event)"
|
|
@@ -5373,29 +7121,51 @@ class PraxisChartWidgetConfigEditor {
|
|
|
5373
7121
|
}
|
|
5374
7122
|
</section>
|
|
5375
7123
|
</section>
|
|
5376
|
-
`, isInline: true, styles: [".chart-widget-config-editor__runtime{display:grid;gap:8px;margin-top:16px}.chart-widget-config-editor__label{font-size:12px;font-weight:600;color:#000000b8}.chart-widget-config-editor__textarea{box-sizing:border-box;width:100%;min-height:112px;padding:10px 12px;border:1px solid rgba(0,0,0,.2);border-radius:6px;font:12px/1.45 ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,monospace;resize:vertical}.chart-widget-config-editor__error{color:#b3261e;font-size:12px}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$3.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$3.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$3.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: PraxisChartConfigEditor, selector: "praxis-chart-config-editor", inputs: ["document", "mode", "readonly", "availableResources", "availableFields", "availableTargets"], outputs: ["apply", "save", "resetChange", "documentChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
7124
|
+
`, isInline: true, styles: [".chart-widget-config-editor__runtime{display:grid;gap:8px;margin-top:16px}.chart-widget-config-editor__diagnostics{display:grid;gap:6px;margin-bottom:16px;padding:12px 14px;border:1px solid color-mix(in srgb,var(--md-sys-color-tertiary, #765b00) 28%,transparent);border-radius:10px;background:color-mix(in srgb,var(--md-sys-color-tertiary-container, #ffdf92) 42%,var(--md-sys-color-surface, #fff));color:var(--md-sys-color-on-tertiary-container, #271900);font-size:12px;line-height:1.4}.chart-widget-config-editor__diagnostics ul{display:grid;gap:4px;margin:0;padding-left:18px}.chart-widget-config-editor__diagnostic--error{color:var(--md-sys-color-error, #b3261e)}.chart-widget-config-editor__label{font-size:12px;font-weight:600;color:#000000b8}.chart-widget-config-editor__textarea{box-sizing:border-box;width:100%;min-height:112px;padding:10px 12px;border:1px solid rgba(0,0,0,.2);border-radius:6px;font:12px/1.45 ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,monospace;resize:vertical}.chart-widget-config-editor__error{color:#b3261e;font-size:12px}.chart-widget-config-editor__notice{padding:12px;border:1px solid rgba(0,0,0,.12);border-radius:6px;color:#000000b8;font-size:12px;line-height:1.4;background:#00000008}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$3.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$3.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$3.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: PraxisChartConfigEditor, selector: "praxis-chart-config-editor", inputs: ["document", "mode", "readonly", "availableResources", "availableFields", "availableTargets"], outputs: ["apply", "save", "resetChange", "documentChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
5377
7125
|
}
|
|
5378
7126
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: PraxisChartWidgetConfigEditor, decorators: [{
|
|
5379
7127
|
type: Component,
|
|
5380
7128
|
args: [{ selector: 'praxis-chart-widget-config-editor', standalone: true, imports: [FormsModule, PraxisChartConfigEditor], template: `
|
|
5381
7129
|
<section data-testid="chart-widget-config-editor">
|
|
5382
|
-
|
|
5383
|
-
|
|
5384
|
-
|
|
5385
|
-
|
|
5386
|
-
|
|
5387
|
-
|
|
5388
|
-
|
|
7130
|
+
@if (contextDiagnostics.length) {
|
|
7131
|
+
<section
|
|
7132
|
+
class="chart-widget-config-editor__diagnostics"
|
|
7133
|
+
data-testid="chart-widget-context-diagnostics"
|
|
7134
|
+
role="status"
|
|
7135
|
+
>
|
|
7136
|
+
<strong>{{ t('praxis.charts.widget.contextDiagnostics.title', 'Authoring context') }}</strong>
|
|
7137
|
+
<ul>
|
|
7138
|
+
@for (diagnostic of contextDiagnostics; track diagnostic.code + ':' + (diagnostic.path || '')) {
|
|
7139
|
+
<li [class]="'chart-widget-config-editor__diagnostic--' + (diagnostic.severity || 'info')">
|
|
7140
|
+
{{ contextDiagnosticText(diagnostic) }}
|
|
7141
|
+
</li>
|
|
7142
|
+
}
|
|
7143
|
+
</ul>
|
|
7144
|
+
</section>
|
|
7145
|
+
}
|
|
7146
|
+
@if (chartDocument) {
|
|
7147
|
+
<praxis-chart-config-editor
|
|
7148
|
+
#chartEditor
|
|
7149
|
+
[document]="chartDocument"
|
|
7150
|
+
[availableResources]="availableResources"
|
|
7151
|
+
[availableFields]="availableFields"
|
|
7152
|
+
[availableTargets]="availableTargets"
|
|
7153
|
+
/>
|
|
7154
|
+
} @else {
|
|
7155
|
+
<section class="chart-widget-config-editor__notice" data-testid="chart-widget-missing-document">
|
|
7156
|
+
{{ t('praxis.charts.widget.missingDocument', 'This widget does not have a canonical chart document yet. Runtime inputs are preserved until a canonical chartDocument is provided by the host.') }}
|
|
7157
|
+
</section>
|
|
7158
|
+
}
|
|
5389
7159
|
<section class="chart-widget-config-editor__runtime">
|
|
5390
7160
|
<label class="chart-widget-config-editor__label" for="chart-query-context">
|
|
5391
|
-
Query context
|
|
7161
|
+
{{ t('praxis.charts.widget.queryContext.label', 'Query context') }}
|
|
5392
7162
|
</label>
|
|
5393
7163
|
<textarea
|
|
5394
7164
|
id="chart-query-context"
|
|
5395
7165
|
class="chart-widget-config-editor__textarea"
|
|
5396
7166
|
rows="5"
|
|
5397
7167
|
spellcheck="false"
|
|
5398
|
-
aria-label="
|
|
7168
|
+
[attr.aria-label]="t('praxis.charts.widget.queryContext.ariaLabel', 'Chart query context')"
|
|
5399
7169
|
data-testid="chart-widget-query-context"
|
|
5400
7170
|
[ngModel]="queryContextText"
|
|
5401
7171
|
(ngModelChange)="setQueryContextText($event)"
|
|
@@ -5407,9 +7177,13 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
|
|
|
5407
7177
|
}
|
|
5408
7178
|
</section>
|
|
5409
7179
|
</section>
|
|
5410
|
-
`, changeDetection: ChangeDetectionStrategy.OnPush, styles: [".chart-widget-config-editor__runtime{display:grid;gap:8px;margin-top:16px}.chart-widget-config-editor__label{font-size:12px;font-weight:600;color:#000000b8}.chart-widget-config-editor__textarea{box-sizing:border-box;width:100%;min-height:112px;padding:10px 12px;border:1px solid rgba(0,0,0,.2);border-radius:6px;font:12px/1.45 ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,monospace;resize:vertical}.chart-widget-config-editor__error{color:#b3261e;font-size:12px}\n"] }]
|
|
7180
|
+
`, changeDetection: ChangeDetectionStrategy.OnPush, providers: [providePraxisChartsI18n()], styles: [".chart-widget-config-editor__runtime{display:grid;gap:8px;margin-top:16px}.chart-widget-config-editor__diagnostics{display:grid;gap:6px;margin-bottom:16px;padding:12px 14px;border:1px solid color-mix(in srgb,var(--md-sys-color-tertiary, #765b00) 28%,transparent);border-radius:10px;background:color-mix(in srgb,var(--md-sys-color-tertiary-container, #ffdf92) 42%,var(--md-sys-color-surface, #fff));color:var(--md-sys-color-on-tertiary-container, #271900);font-size:12px;line-height:1.4}.chart-widget-config-editor__diagnostics ul{display:grid;gap:4px;margin:0;padding-left:18px}.chart-widget-config-editor__diagnostic--error{color:var(--md-sys-color-error, #b3261e)}.chart-widget-config-editor__label{font-size:12px;font-weight:600;color:#000000b8}.chart-widget-config-editor__textarea{box-sizing:border-box;width:100%;min-height:112px;padding:10px 12px;border:1px solid rgba(0,0,0,.2);border-radius:6px;font:12px/1.45 ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,monospace;resize:vertical}.chart-widget-config-editor__error{color:#b3261e;font-size:12px}.chart-widget-config-editor__notice{padding:12px;border:1px solid rgba(0,0,0,.12);border-radius:6px;color:#000000b8;font-size:12px;line-height:1.4;background:#00000008}\n"] }]
|
|
5411
7181
|
}], propDecorators: { inputs: [{
|
|
5412
7182
|
type: Input
|
|
7183
|
+
}], context: [{
|
|
7184
|
+
type: Input
|
|
7185
|
+
}], contextDiagnostics: [{
|
|
7186
|
+
type: Input
|
|
5413
7187
|
}], chartEditor: [{
|
|
5414
7188
|
type: ViewChild,
|
|
5415
7189
|
args: ['chartEditor']
|
|
@@ -5441,6 +7215,25 @@ const PRAXIS_CHART_PALETTE_DEFAULT_DATA = [
|
|
|
5441
7215
|
{ category: 'B', value: 32 },
|
|
5442
7216
|
{ category: 'C', value: 24 },
|
|
5443
7217
|
];
|
|
7218
|
+
const PRAXIS_CHART_PALETTE_DEFAULT_DOCUMENT = {
|
|
7219
|
+
version: PRAXIS_X_UI_CHART_AUTHORABLE_VERSION,
|
|
7220
|
+
kind: 'bar',
|
|
7221
|
+
chartId: 'palette-chart',
|
|
7222
|
+
title: 'Analytics preview',
|
|
7223
|
+
subtitle: 'Sample chart inserted from the Praxis component palette.',
|
|
7224
|
+
source: {
|
|
7225
|
+
kind: 'derived',
|
|
7226
|
+
},
|
|
7227
|
+
dimensions: [{ field: 'category', role: 'category' }],
|
|
7228
|
+
metrics: [{ field: 'value', aggregation: 'sum', label: 'Value' }],
|
|
7229
|
+
legend: { enabled: true },
|
|
7230
|
+
labels: { enabled: false },
|
|
7231
|
+
tooltip: { enabled: true },
|
|
7232
|
+
motion: {
|
|
7233
|
+
enabled: true,
|
|
7234
|
+
preset: 'standard',
|
|
7235
|
+
},
|
|
7236
|
+
};
|
|
5444
7237
|
const PRAXIS_CHART_PHASE1_PORTS = [
|
|
5445
7238
|
{
|
|
5446
7239
|
id: 'queryContext',
|
|
@@ -5503,7 +7296,7 @@ const PRAXIS_CHART_PHASE1_PORTS = [
|
|
|
5503
7296
|
label: 'Point Click',
|
|
5504
7297
|
direction: 'output',
|
|
5505
7298
|
semanticKind: 'event',
|
|
5506
|
-
description: '
|
|
7299
|
+
description: 'Raw renderer-neutral point evidence emitted when a point or series is clicked.',
|
|
5507
7300
|
cardinality: 'stream',
|
|
5508
7301
|
schema: {
|
|
5509
7302
|
id: 'PraxisChartPointEvent',
|
|
@@ -5512,6 +7305,20 @@ const PRAXIS_CHART_PHASE1_PORTS = [
|
|
|
5512
7305
|
},
|
|
5513
7306
|
exposure: { public: true, group: 'interaction' },
|
|
5514
7307
|
},
|
|
7308
|
+
{
|
|
7309
|
+
id: 'pointAction',
|
|
7310
|
+
label: 'Point Action',
|
|
7311
|
+
direction: 'output',
|
|
7312
|
+
semanticKind: 'event',
|
|
7313
|
+
description: 'Structured configured pointClick action derived from raw point evidence.',
|
|
7314
|
+
cardinality: 'stream',
|
|
7315
|
+
schema: {
|
|
7316
|
+
id: 'PraxisChartPointActionEvent',
|
|
7317
|
+
kind: 'ts-type',
|
|
7318
|
+
ref: 'PraxisChartPointActionEvent',
|
|
7319
|
+
},
|
|
7320
|
+
exposure: { public: true, group: 'interaction' },
|
|
7321
|
+
},
|
|
5515
7322
|
{
|
|
5516
7323
|
id: 'selectionChange',
|
|
5517
7324
|
label: 'Selection Change',
|
|
@@ -5526,6 +7333,20 @@ const PRAXIS_CHART_PHASE1_PORTS = [
|
|
|
5526
7333
|
},
|
|
5527
7334
|
exposure: { public: true, group: 'interaction' },
|
|
5528
7335
|
},
|
|
7336
|
+
{
|
|
7337
|
+
id: 'drillDown',
|
|
7338
|
+
label: 'Drill Down',
|
|
7339
|
+
direction: 'output',
|
|
7340
|
+
semanticKind: 'event',
|
|
7341
|
+
description: 'Structured drill-down action payload derived from selected chart point evidence.',
|
|
7342
|
+
cardinality: 'stream',
|
|
7343
|
+
schema: {
|
|
7344
|
+
id: 'PraxisChartDrillDownEvent',
|
|
7345
|
+
kind: 'ts-type',
|
|
7346
|
+
ref: 'PraxisChartDrillDownEvent',
|
|
7347
|
+
},
|
|
7348
|
+
exposure: { public: true, group: 'interaction' },
|
|
7349
|
+
},
|
|
5529
7350
|
{
|
|
5530
7351
|
id: 'crossFilter',
|
|
5531
7352
|
label: 'Cross Filter',
|
|
@@ -5584,6 +7405,23 @@ const PRAXIS_CHART_COMPONENT_METADATA = {
|
|
|
5584
7405
|
component: PraxisChartWidgetConfigEditor,
|
|
5585
7406
|
title: 'Configure chart',
|
|
5586
7407
|
},
|
|
7408
|
+
authoringManifestRef: {
|
|
7409
|
+
componentId: 'praxis-chart',
|
|
7410
|
+
source: 'PRAXIS_CHARTS_AUTHORING_MANIFEST',
|
|
7411
|
+
},
|
|
7412
|
+
insertionPresets: [
|
|
7413
|
+
{
|
|
7414
|
+
id: 'canonical-chart',
|
|
7415
|
+
label: 'Canonical chart',
|
|
7416
|
+
description: 'Creates a chart widget from the canonical x-ui.chart authoring document.',
|
|
7417
|
+
icon: 'bar_chart',
|
|
7418
|
+
inputs: {
|
|
7419
|
+
chartDocument: PRAXIS_CHART_PALETTE_DEFAULT_DOCUMENT,
|
|
7420
|
+
data: PRAXIS_CHART_PALETTE_DEFAULT_DATA,
|
|
7421
|
+
enableCustomization: true,
|
|
7422
|
+
},
|
|
7423
|
+
},
|
|
7424
|
+
],
|
|
5587
7425
|
inputs: [
|
|
5588
7426
|
{
|
|
5589
7427
|
name: 'config',
|
|
@@ -5601,6 +7439,7 @@ const PRAXIS_CHART_COMPONENT_METADATA = {
|
|
|
5601
7439
|
name: 'chartDocument',
|
|
5602
7440
|
type: 'PraxisXUiChartContract | null',
|
|
5603
7441
|
description: 'Optional canonical x-ui.chart document used as the authoring source of truth for settings-panel integration.',
|
|
7442
|
+
default: PRAXIS_CHART_PALETTE_DEFAULT_DOCUMENT,
|
|
5604
7443
|
},
|
|
5605
7444
|
{
|
|
5606
7445
|
name: 'queryContext',
|
|
@@ -5626,19 +7465,19 @@ const PRAXIS_CHART_COMPONENT_METADATA = {
|
|
|
5626
7465
|
{
|
|
5627
7466
|
name: 'availableResources',
|
|
5628
7467
|
type: 'ReadonlyArray<ChartEditorResourceOption>',
|
|
5629
|
-
description: '
|
|
7468
|
+
description: 'Transient governed resource catalog. id is the stable discovery identity and path is the root-relative operational value persisted to chartDocument.source.resource; the catalog itself is not persisted.',
|
|
5630
7469
|
default: [],
|
|
5631
7470
|
},
|
|
5632
7471
|
{
|
|
5633
7472
|
name: 'availableFields',
|
|
5634
7473
|
type: 'ReadonlyArray<ChartEditorFieldOption>',
|
|
5635
|
-
description: '
|
|
7474
|
+
description: 'Transient capability-backed field catalog with exact operation, aggregation and distribution terms/histogram eligibility; not intended for widget persistence.',
|
|
5636
7475
|
default: [],
|
|
5637
7476
|
},
|
|
5638
7477
|
{
|
|
5639
7478
|
name: 'availableTargets',
|
|
5640
7479
|
type: 'ReadonlyArray<ChartEditorTargetOption>',
|
|
5641
|
-
description: '
|
|
7480
|
+
description: 'Transient event-scoped targets from existing top-level links that are compatible with public, non-deprecated widget ports or explicitly writable declared page state. events[] identifies the exact authorized structured Chart events; raw pointClick never authorizes events.pointClick, which uses pointAction. Local transform.output semantic-kind inspection is provisional and does not replace a Core-validated transform-output projection (P2). The catalog is not persisted.',
|
|
5642
7481
|
default: [],
|
|
5643
7482
|
},
|
|
5644
7483
|
],
|
|
@@ -5646,12 +7485,22 @@ const PRAXIS_CHART_COMPONENT_METADATA = {
|
|
|
5646
7485
|
{
|
|
5647
7486
|
name: 'pointClick',
|
|
5648
7487
|
type: 'PraxisChartPointEvent',
|
|
5649
|
-
description: '
|
|
7488
|
+
description: 'Raw renderer-neutral point evidence emitted when the host wants to observe a point/series click.',
|
|
7489
|
+
},
|
|
7490
|
+
{
|
|
7491
|
+
name: 'pointAction',
|
|
7492
|
+
type: 'PraxisChartPointActionEvent',
|
|
7493
|
+
description: 'Emitted when a configured pointClick action resolves semantic filters and target evidence from a point/series click.',
|
|
5650
7494
|
},
|
|
5651
7495
|
{
|
|
5652
7496
|
name: 'selectionChange',
|
|
5653
7497
|
type: 'PraxisChartSelectionEvent',
|
|
5654
|
-
description: 'Emitted when declarative chart selection resolves
|
|
7498
|
+
description: 'Emitted when declarative single-point chart selection resolves selected point evidence and canonical filters.',
|
|
7499
|
+
},
|
|
7500
|
+
{
|
|
7501
|
+
name: 'drillDown',
|
|
7502
|
+
type: 'PraxisChartDrillDownEvent',
|
|
7503
|
+
description: 'Emitted when declarative drill-down resolves action, target and canonical filters from a selected chart point.',
|
|
5655
7504
|
},
|
|
5656
7505
|
{
|
|
5657
7506
|
name: 'crossFilter',
|
|
@@ -5695,10 +7544,18 @@ function providePraxisChartsMetadata() {
|
|
|
5695
7544
|
return {
|
|
5696
7545
|
provide: ENVIRONMENT_INITIALIZER,
|
|
5697
7546
|
multi: true,
|
|
5698
|
-
useFactory: (registry) => () => {
|
|
5699
|
-
registry.register(
|
|
7547
|
+
useFactory: (registry, injector) => () => {
|
|
7548
|
+
registry.register({
|
|
7549
|
+
...PRAXIS_CHART_COMPONENT_METADATA,
|
|
7550
|
+
configEditor: {
|
|
7551
|
+
...PRAXIS_CHART_COMPONENT_METADATA.configEditor,
|
|
7552
|
+
contextResolver: (request) => injector
|
|
7553
|
+
.get(PraxisChartConfigEditorContextResolverService)
|
|
7554
|
+
.resolve(request),
|
|
7555
|
+
},
|
|
7556
|
+
});
|
|
5700
7557
|
},
|
|
5701
|
-
deps: [ComponentMetadataRegistry],
|
|
7558
|
+
deps: [ComponentMetadataRegistry, Injector],
|
|
5702
7559
|
};
|
|
5703
7560
|
}
|
|
5704
7561
|
|
|
@@ -5780,7 +7637,9 @@ class PraxisChartShowcaseWidgetComponent {
|
|
|
5780
7637
|
enableCustomization = input(false, ...(ngDevMode ? [{ debugName: "enableCustomization" }] : /* istanbul ignore next */ []));
|
|
5781
7638
|
viewMode = input('chart', ...(ngDevMode ? [{ debugName: "viewMode" }] : /* istanbul ignore next */ []));
|
|
5782
7639
|
pointClick = output();
|
|
7640
|
+
pointAction = output();
|
|
5783
7641
|
selectionChange = output();
|
|
7642
|
+
drillDown = output();
|
|
5784
7643
|
crossFilter = output();
|
|
5785
7644
|
queryRequest = output();
|
|
5786
7645
|
loadStateChange = output();
|
|
@@ -5896,7 +7755,7 @@ class PraxisChartShowcaseWidgetComponent {
|
|
|
5896
7755
|
});
|
|
5897
7756
|
}
|
|
5898
7757
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: PraxisChartShowcaseWidgetComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
5899
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.14", type: PraxisChartShowcaseWidgetComponent, isStandalone: true, selector: "praxis-chart-showcase-widget", inputs: { config: { classPropertyName: "config", publicName: "config", isSignal: true, isRequired: true, transformFunction: null }, data: { classPropertyName: "data", publicName: "data", isSignal: true, isRequired: false, transformFunction: null }, chartDocument: { classPropertyName: "chartDocument", publicName: "chartDocument", isSignal: true, isRequired: false, transformFunction: null }, remoteDataResolver: { classPropertyName: "remoteDataResolver", publicName: "remoteDataResolver", isSignal: true, isRequired: false, transformFunction: null }, enableCustomization: { classPropertyName: "enableCustomization", publicName: "enableCustomization", isSignal: true, isRequired: false, transformFunction: null }, viewMode: { classPropertyName: "viewMode", publicName: "viewMode", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { pointClick: "pointClick", selectionChange: "selectionChange", crossFilter: "crossFilter", queryRequest: "queryRequest", loadStateChange: "loadStateChange" }, ngImport: i0, template: `
|
|
7758
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.14", type: PraxisChartShowcaseWidgetComponent, isStandalone: true, selector: "praxis-chart-showcase-widget", inputs: { config: { classPropertyName: "config", publicName: "config", isSignal: true, isRequired: true, transformFunction: null }, data: { classPropertyName: "data", publicName: "data", isSignal: true, isRequired: false, transformFunction: null }, chartDocument: { classPropertyName: "chartDocument", publicName: "chartDocument", isSignal: true, isRequired: false, transformFunction: null }, remoteDataResolver: { classPropertyName: "remoteDataResolver", publicName: "remoteDataResolver", isSignal: true, isRequired: false, transformFunction: null }, enableCustomization: { classPropertyName: "enableCustomization", publicName: "enableCustomization", isSignal: true, isRequired: false, transformFunction: null }, viewMode: { classPropertyName: "viewMode", publicName: "viewMode", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { pointClick: "pointClick", pointAction: "pointAction", selectionChange: "selectionChange", drillDown: "drillDown", crossFilter: "crossFilter", queryRequest: "queryRequest", loadStateChange: "loadStateChange" }, ngImport: i0, template: `
|
|
5900
7759
|
@if (viewMode() === 'chart') {
|
|
5901
7760
|
<praxis-chart
|
|
5902
7761
|
[config]="config()"
|
|
@@ -5905,7 +7764,9 @@ class PraxisChartShowcaseWidgetComponent {
|
|
|
5905
7764
|
[remoteDataResolver]="remoteDataResolver()"
|
|
5906
7765
|
[enableCustomization]="enableCustomization()"
|
|
5907
7766
|
(pointClick)="pointClick.emit($event)"
|
|
7767
|
+
(pointAction)="pointAction.emit($event)"
|
|
5908
7768
|
(selectionChange)="selectionChange.emit($event)"
|
|
7769
|
+
(drillDown)="drillDown.emit($event)"
|
|
5909
7770
|
(crossFilter)="crossFilter.emit($event)"
|
|
5910
7771
|
(queryRequest)="queryRequest.emit($event)"
|
|
5911
7772
|
(loadStateChange)="loadStateChange.emit($event)"
|
|
@@ -5932,7 +7793,7 @@ class PraxisChartShowcaseWidgetComponent {
|
|
|
5932
7793
|
(rowClick)="handleRowClick($event)"
|
|
5933
7794
|
></praxis-table>
|
|
5934
7795
|
}
|
|
5935
|
-
`, isInline: true, styles: [":host{display:block;height:100%;min-width:0}.showcase-state-card{min-height:240px;display:grid;place-content:center;gap:8px;padding:24px;border-radius:18px;border:1px solid color-mix(in srgb,var(--md-sys-color-outline, #c5c7ce) 72%,transparent);background:linear-gradient(180deg,#fffffff5,#f4f7fbfa);text-align:center}.showcase-state-card h4{margin:0;font-size:1rem;font-weight:600;color:var(--md-sys-color-on-surface, #1a1b20)}.showcase-state-card p{margin:0;color:var(--md-sys-color-on-surface-variant, #5a5d67)}\n"], dependencies: [{ kind: "component", type: PraxisChartComponent, selector: "praxis-chart", inputs: ["config", "data", "chartDocument", "filterCriteria", "queryContext", "remoteDataResolver", "enableCustomization", "availableResources", "availableFields", "availableTargets"], outputs: ["pointClick", "selectionChange", "crossFilter", "queryRequest", "loadStateChange", "chartDocumentApplied", "chartDocumentSaved"] }, { kind: "component", type: PraxisTable, selector: "praxis-table", inputs: ["config", "resourcePath", "data", "tableId", "componentInstanceId", "configPersistenceStrategy", "title", "subtitle", "icon", "autoDelete", "notifyIfOutdated", "snoozeMs", "autoOpenSettingsOnOutdated", "crudContext", "filterCriteria", "queryContext", "aiContext", "aiAssistantVoiceInputMode", "aiAssistantVoiceLanguage", "horizontalScroll", "enableCustomization", "authoringCapability", "dense"], outputs: ["rowClick", "widgetEvent", "resourceEvent", "rowDoubleClick", "rowExpansionChange", "rowAction", "toolbarAction", "bulkAction", "exportAction", "columnReorder", "columnReorderAttempt", "columnResize", "beforeDelete", "afterDelete", "deleteError", "beforeBulkDelete", "afterBulkDelete", "bulkDeleteError", "schemaStatusChange", "configChange", "metadataChange", "loadingStateChange", "collectionLinksChange", "selectionChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
7796
|
+
`, isInline: true, styles: [":host{display:block;height:100%;min-width:0}.showcase-state-card{min-height:240px;display:grid;place-content:center;gap:8px;padding:24px;border-radius:18px;border:1px solid color-mix(in srgb,var(--md-sys-color-outline, #c5c7ce) 72%,transparent);background:linear-gradient(180deg,#fffffff5,#f4f7fbfa);text-align:center}.showcase-state-card h4{margin:0;font-size:1rem;font-weight:600;color:var(--md-sys-color-on-surface, #1a1b20)}.showcase-state-card p{margin:0;color:var(--md-sys-color-on-surface-variant, #5a5d67)}\n"], dependencies: [{ kind: "component", type: PraxisChartComponent, selector: "praxis-chart", inputs: ["config", "data", "chartDocument", "filterCriteria", "queryContext", "remoteDataResolver", "enableCustomization", "availableResources", "availableFields", "availableTargets"], outputs: ["pointClick", "pointAction", "selectionChange", "drillDown", "crossFilter", "queryRequest", "loadStateChange", "chartDocumentApplied", "chartDocumentSaved"] }, { kind: "component", type: PraxisTable, selector: "praxis-table", inputs: ["config", "resourcePath", "data", "tableId", "componentInstanceId", "configPersistenceStrategy", "title", "subtitle", "icon", "autoDelete", "notifyIfOutdated", "snoozeMs", "autoOpenSettingsOnOutdated", "crudContext", "filterCriteria", "queryContext", "aiContext", "aiAssistantVoiceInputMode", "aiAssistantVoiceLanguage", "horizontalScroll", "enableCustomization", "authoringCapability", "dense"], outputs: ["rowClick", "widgetEvent", "resourceEvent", "rowDoubleClick", "rowExpansionChange", "rowAction", "toolbarAction", "bulkAction", "exportAction", "columnReorder", "columnReorderAttempt", "columnResize", "beforeDelete", "afterDelete", "deleteError", "beforeBulkDelete", "afterBulkDelete", "bulkDeleteError", "schemaStatusChange", "configChange", "metadataChange", "loadingStateChange", "collectionLinksChange", "selectionChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
5936
7797
|
}
|
|
5937
7798
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: PraxisChartShowcaseWidgetComponent, decorators: [{
|
|
5938
7799
|
type: Component,
|
|
@@ -5945,7 +7806,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
|
|
|
5945
7806
|
[remoteDataResolver]="remoteDataResolver()"
|
|
5946
7807
|
[enableCustomization]="enableCustomization()"
|
|
5947
7808
|
(pointClick)="pointClick.emit($event)"
|
|
7809
|
+
(pointAction)="pointAction.emit($event)"
|
|
5948
7810
|
(selectionChange)="selectionChange.emit($event)"
|
|
7811
|
+
(drillDown)="drillDown.emit($event)"
|
|
5949
7812
|
(crossFilter)="crossFilter.emit($event)"
|
|
5950
7813
|
(queryRequest)="queryRequest.emit($event)"
|
|
5951
7814
|
(loadStateChange)="loadStateChange.emit($event)"
|
|
@@ -5973,7 +7836,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
|
|
|
5973
7836
|
></praxis-table>
|
|
5974
7837
|
}
|
|
5975
7838
|
`, styles: [":host{display:block;height:100%;min-width:0}.showcase-state-card{min-height:240px;display:grid;place-content:center;gap:8px;padding:24px;border-radius:18px;border:1px solid color-mix(in srgb,var(--md-sys-color-outline, #c5c7ce) 72%,transparent);background:linear-gradient(180deg,#fffffff5,#f4f7fbfa);text-align:center}.showcase-state-card h4{margin:0;font-size:1rem;font-weight:600;color:var(--md-sys-color-on-surface, #1a1b20)}.showcase-state-card p{margin:0;color:var(--md-sys-color-on-surface-variant, #5a5d67)}\n"] }]
|
|
5976
|
-
}], ctorParameters: () => [], propDecorators: { config: [{ type: i0.Input, args: [{ isSignal: true, alias: "config", required: true }] }], data: [{ type: i0.Input, args: [{ isSignal: true, alias: "data", required: false }] }], chartDocument: [{ type: i0.Input, args: [{ isSignal: true, alias: "chartDocument", required: false }] }], remoteDataResolver: [{ type: i0.Input, args: [{ isSignal: true, alias: "remoteDataResolver", required: false }] }], enableCustomization: [{ type: i0.Input, args: [{ isSignal: true, alias: "enableCustomization", required: false }] }], viewMode: [{ type: i0.Input, args: [{ isSignal: true, alias: "viewMode", required: false }] }], pointClick: [{ type: i0.Output, args: ["pointClick"] }], selectionChange: [{ type: i0.Output, args: ["selectionChange"] }], crossFilter: [{ type: i0.Output, args: ["crossFilter"] }], queryRequest: [{ type: i0.Output, args: ["queryRequest"] }], loadStateChange: [{ type: i0.Output, args: ["loadStateChange"] }] } });
|
|
7839
|
+
}], ctorParameters: () => [], propDecorators: { config: [{ type: i0.Input, args: [{ isSignal: true, alias: "config", required: true }] }], data: [{ type: i0.Input, args: [{ isSignal: true, alias: "data", required: false }] }], chartDocument: [{ type: i0.Input, args: [{ isSignal: true, alias: "chartDocument", required: false }] }], remoteDataResolver: [{ type: i0.Input, args: [{ isSignal: true, alias: "remoteDataResolver", required: false }] }], enableCustomization: [{ type: i0.Input, args: [{ isSignal: true, alias: "enableCustomization", required: false }] }], viewMode: [{ type: i0.Input, args: [{ isSignal: true, alias: "viewMode", required: false }] }], pointClick: [{ type: i0.Output, args: ["pointClick"] }], pointAction: [{ type: i0.Output, args: ["pointAction"] }], selectionChange: [{ type: i0.Output, args: ["selectionChange"] }], drillDown: [{ type: i0.Output, args: ["drillDown"] }], crossFilter: [{ type: i0.Output, args: ["crossFilter"] }], queryRequest: [{ type: i0.Output, args: ["queryRequest"] }], loadStateChange: [{ type: i0.Output, args: ["loadStateChange"] }] } });
|
|
5977
7840
|
function resolveShowcaseStateMode(config, remoteRuntimeState = 'idle') {
|
|
5978
7841
|
if (config.preferredLoadState === 'loading') {
|
|
5979
7842
|
return 'loading';
|
|
@@ -6152,13 +8015,23 @@ const PRAXIS_CHART_SHOWCASE_WIDGET_METADATA = {
|
|
|
6152
8015
|
{
|
|
6153
8016
|
name: 'pointClick',
|
|
6154
8017
|
type: 'PraxisChartPointEvent',
|
|
6155
|
-
description: 'Forwards point
|
|
8018
|
+
description: 'Forwards raw point evidence from the chart or mapped row selection from the table.',
|
|
8019
|
+
},
|
|
8020
|
+
{
|
|
8021
|
+
name: 'pointAction',
|
|
8022
|
+
type: 'PraxisChartPointActionEvent',
|
|
8023
|
+
description: 'Forwards configured pointClick action payloads from the chart.',
|
|
6156
8024
|
},
|
|
6157
8025
|
{
|
|
6158
8026
|
name: 'selectionChange',
|
|
6159
8027
|
type: 'PraxisChartSelectionEvent',
|
|
6160
8028
|
description: 'Forwards chart selection events with mapped filter values.',
|
|
6161
8029
|
},
|
|
8030
|
+
{
|
|
8031
|
+
name: 'drillDown',
|
|
8032
|
+
type: 'PraxisChartDrillDownEvent',
|
|
8033
|
+
description: 'Forwards chart drill-down action payloads for Dynamic Page composition links.',
|
|
8034
|
+
},
|
|
6162
8035
|
{
|
|
6163
8036
|
name: 'crossFilter',
|
|
6164
8037
|
type: 'PraxisChartCrossFilterEvent',
|
|
@@ -6211,7 +8084,7 @@ function provideChartEngineFactory(options) {
|
|
|
6211
8084
|
return {
|
|
6212
8085
|
provide: PRAXIS_CHART_ENGINE_FACTORY,
|
|
6213
8086
|
useFactory: (optionBuilder) => () => new EChartsEngineAdapter(optionBuilder),
|
|
6214
|
-
deps: [
|
|
8087
|
+
deps: [EChartsOptionBuilderService],
|
|
6215
8088
|
};
|
|
6216
8089
|
}
|
|
6217
8090
|
function providePraxisCharts(options = {}) {
|
|
@@ -6229,38 +8102,10 @@ function providePraxisCharts(options = {}) {
|
|
|
6229
8102
|
];
|
|
6230
8103
|
}
|
|
6231
8104
|
|
|
6232
|
-
class PraxisChartMetadataRegistrationService {
|
|
6233
|
-
registry;
|
|
6234
|
-
registered = false;
|
|
6235
|
-
constructor(registry) {
|
|
6236
|
-
this.registry = registry;
|
|
6237
|
-
this.ensureRegistered();
|
|
6238
|
-
}
|
|
6239
|
-
ensureRegistered() {
|
|
6240
|
-
if (this.registered) {
|
|
6241
|
-
return;
|
|
6242
|
-
}
|
|
6243
|
-
this.registry.register(PRAXIS_CHART_COMPONENT_METADATA);
|
|
6244
|
-
this.registry.register(PRAXIS_CHART_DRILLDOWN_PANEL_METADATA);
|
|
6245
|
-
this.registry.register(PRAXIS_CHART_STATE_PROBE_COMPONENT_METADATA);
|
|
6246
|
-
this.registry.register(PRAXIS_CHART_SHOWCASE_WIDGET_METADATA);
|
|
6247
|
-
this.registered = true;
|
|
6248
|
-
}
|
|
6249
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: PraxisChartMetadataRegistrationService, deps: [{ token: i1$1.ComponentMetadataRegistry }], target: i0.ɵɵFactoryTarget.Injectable });
|
|
6250
|
-
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: PraxisChartMetadataRegistrationService, providedIn: 'root' });
|
|
6251
|
-
}
|
|
6252
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: PraxisChartMetadataRegistrationService, decorators: [{
|
|
6253
|
-
type: Injectable,
|
|
6254
|
-
args: [{ providedIn: 'root' }]
|
|
6255
|
-
}], ctorParameters: () => [{ type: i1$1.ComponentMetadataRegistry }] });
|
|
6256
|
-
|
|
6257
8105
|
class PraxisChartSchemaMapperService {
|
|
6258
|
-
metadataRegistration;
|
|
6259
8106
|
canonicalContractMapper;
|
|
6260
|
-
constructor(
|
|
6261
|
-
this.metadataRegistration = metadataRegistration;
|
|
8107
|
+
constructor(canonicalContractMapper) {
|
|
6262
8108
|
this.canonicalContractMapper = canonicalContractMapper;
|
|
6263
|
-
this.metadataRegistration.ensureRegistered();
|
|
6264
8109
|
}
|
|
6265
8110
|
resolve(input, defaults) {
|
|
6266
8111
|
const schema = this.normalize(input);
|
|
@@ -6349,8 +8194,12 @@ class PraxisChartSchemaMapperService {
|
|
|
6349
8194
|
}
|
|
6350
8195
|
const events = schema.chart?.events;
|
|
6351
8196
|
const outputs = {};
|
|
6352
|
-
if (events?.pointClick
|
|
8197
|
+
if (events?.pointClick) {
|
|
6353
8198
|
outputs['pointClick'] = 'emit';
|
|
8199
|
+
outputs['pointAction'] = 'emit';
|
|
8200
|
+
}
|
|
8201
|
+
if (events?.drillDown) {
|
|
8202
|
+
outputs['drillDown'] = 'emit';
|
|
6354
8203
|
}
|
|
6355
8204
|
if (events?.selectionChange) {
|
|
6356
8205
|
outputs['selectionChange'] = 'emit';
|
|
@@ -6360,13 +8209,13 @@ class PraxisChartSchemaMapperService {
|
|
|
6360
8209
|
}
|
|
6361
8210
|
return Object.keys(outputs).length ? outputs : undefined;
|
|
6362
8211
|
}
|
|
6363
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: PraxisChartSchemaMapperService, deps: [{ token:
|
|
8212
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: PraxisChartSchemaMapperService, deps: [{ token: PraxisChartCanonicalContractMapperService }], target: i0.ɵɵFactoryTarget.Injectable });
|
|
6364
8213
|
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: PraxisChartSchemaMapperService, providedIn: 'root' });
|
|
6365
8214
|
}
|
|
6366
8215
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: PraxisChartSchemaMapperService, decorators: [{
|
|
6367
8216
|
type: Injectable,
|
|
6368
8217
|
args: [{ providedIn: 'root' }]
|
|
6369
|
-
}], ctorParameters: () => [{ type:
|
|
8218
|
+
}], ctorParameters: () => [{ type: PraxisChartCanonicalContractMapperService }] });
|
|
6370
8219
|
|
|
6371
8220
|
class PraxisChartBackendPayloadAdapterService {
|
|
6372
8221
|
chartSchemaMapper;
|
|
@@ -6420,7 +8269,11 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
|
|
|
6420
8269
|
}], ctorParameters: () => [{ type: PraxisChartSchemaMapperService }, { type: PraxisChartCanonicalContractMapperService }] });
|
|
6421
8270
|
|
|
6422
8271
|
class AnalyticsChartConfigAdapterService {
|
|
8272
|
+
i18n;
|
|
6423
8273
|
statsBuilder = new AnalyticsStatsRequestBuilderService();
|
|
8274
|
+
constructor(i18n) {
|
|
8275
|
+
this.i18n = i18n;
|
|
8276
|
+
}
|
|
6424
8277
|
toPraxisChartConfig(projection, options) {
|
|
6425
8278
|
const chartType = this.resolveChartType(projection);
|
|
6426
8279
|
const orientation = chartType === 'horizontal-bar' ? 'horizontal' : undefined;
|
|
@@ -6432,6 +8285,11 @@ class AnalyticsChartConfigAdapterService {
|
|
|
6432
8285
|
if (!metrics.length) {
|
|
6433
8286
|
throw new Error(`AnalyticsChartConfigAdapterService requires at least one metric for projection "${projection.id}".`);
|
|
6434
8287
|
}
|
|
8288
|
+
const crossFilter = Boolean(projection.interactions?.crossFilter);
|
|
8289
|
+
const keyFilterField = dimension.keyFilterField?.trim();
|
|
8290
|
+
if (crossFilter && !keyFilterField) {
|
|
8291
|
+
throw new Error(`AnalyticsChartConfigAdapterService requires primaryDimension.keyFilterField when crossFilter is enabled for projection "${projection.id}".`);
|
|
8292
|
+
}
|
|
6435
8293
|
return {
|
|
6436
8294
|
id: projection.id,
|
|
6437
8295
|
type: chartType,
|
|
@@ -6446,6 +8304,17 @@ class AnalyticsChartConfigAdapterService {
|
|
|
6446
8304
|
pointClick: Boolean(projection.interactions?.pointSelection || projection.interactions?.drillDown),
|
|
6447
8305
|
drillDown: Boolean(projection.interactions?.drillDown),
|
|
6448
8306
|
selection: Boolean(projection.interactions?.pointSelection),
|
|
8307
|
+
crossFilter,
|
|
8308
|
+
...(crossFilter
|
|
8309
|
+
? {
|
|
8310
|
+
eventActions: {
|
|
8311
|
+
crossFilter: {
|
|
8312
|
+
action: 'emit',
|
|
8313
|
+
mapping: { key: keyFilterField },
|
|
8314
|
+
},
|
|
8315
|
+
},
|
|
8316
|
+
}
|
|
8317
|
+
: {}),
|
|
6449
8318
|
},
|
|
6450
8319
|
};
|
|
6451
8320
|
}
|
|
@@ -6477,18 +8346,27 @@ class AnalyticsChartConfigAdapterService {
|
|
|
6477
8346
|
}
|
|
6478
8347
|
buildSeries(projection, chartType) {
|
|
6479
8348
|
const dimension = projection.bindings.primaryDimension;
|
|
6480
|
-
|
|
6481
|
-
|
|
6482
|
-
|
|
6483
|
-
|
|
6484
|
-
|
|
6485
|
-
|
|
6486
|
-
|
|
6487
|
-
aggregation: this.mapAggregation(metric.aggregation),
|
|
6488
|
-
|
|
6489
|
-
|
|
6490
|
-
|
|
6491
|
-
|
|
8349
|
+
const comparison = projection.source.operation === 'comparison';
|
|
8350
|
+
return this.getDisplayMetrics(projection).flatMap((metric, index) => comparison
|
|
8351
|
+
? ['current', 'previous'].map((period) => ({
|
|
8352
|
+
id: `${projection.id}.${metric.field}.${period}`,
|
|
8353
|
+
name: `${metric.label ?? metric.field} (${this.comparisonPeriodLabel(period)})`,
|
|
8354
|
+
type: chartType,
|
|
8355
|
+
categoryField: dimension.field,
|
|
8356
|
+
metric: { field: this.comparisonMetricField(metric.field, period), aggregation: this.mapAggregation(metric.aggregation) },
|
|
8357
|
+
}))
|
|
8358
|
+
: [{
|
|
8359
|
+
id: `${projection.id}.${metric.field}.${index + 1}`,
|
|
8360
|
+
name: metric.label ?? metric.field,
|
|
8361
|
+
type: chartType,
|
|
8362
|
+
categoryField: chartType === 'pie' || chartType === 'donut' ? dimension.field : undefined,
|
|
8363
|
+
metric: {
|
|
8364
|
+
field: metric.field,
|
|
8365
|
+
aggregation: this.mapAggregation(metric.aggregation),
|
|
8366
|
+
label: metric.label ?? undefined,
|
|
8367
|
+
},
|
|
8368
|
+
smooth: chartType === 'line' || chartType === 'area',
|
|
8369
|
+
}]);
|
|
6492
8370
|
}
|
|
6493
8371
|
buildDataSource(projection) {
|
|
6494
8372
|
const executionPlan = this.statsBuilder.buildExecutionPlan(projection);
|
|
@@ -6502,7 +8380,13 @@ class AnalyticsChartConfigAdapterService {
|
|
|
6502
8380
|
statsPath: executionPlan.statsPath,
|
|
6503
8381
|
statsRequest: executionPlan.statsRequest,
|
|
6504
8382
|
dimensions: executionPlan.dimensions,
|
|
6505
|
-
metrics:
|
|
8383
|
+
metrics: projection.source.operation === 'comparison'
|
|
8384
|
+
? this.getDisplayMetrics(projection).flatMap((metric) => ['current', 'previous'].map((period) => ({
|
|
8385
|
+
field: this.comparisonMetricField(metric.field, period),
|
|
8386
|
+
aggregation: this.mapAggregation(metric.aggregation),
|
|
8387
|
+
alias: this.comparisonMetricField(metric.field, period),
|
|
8388
|
+
})))
|
|
8389
|
+
: this.mapExecutionMetrics(executionPlan),
|
|
6506
8390
|
sort: executionPlan.sort,
|
|
6507
8391
|
limit: executionPlan.limit,
|
|
6508
8392
|
},
|
|
@@ -6552,13 +8436,25 @@ class AnalyticsChartConfigAdapterService {
|
|
|
6552
8436
|
...(projection.bindings.secondaryMetrics ?? []),
|
|
6553
8437
|
];
|
|
6554
8438
|
}
|
|
6555
|
-
|
|
8439
|
+
comparisonMetricField(metricField, period) {
|
|
8440
|
+
return `__praxisComparison_${metricField}_${period}`;
|
|
8441
|
+
}
|
|
8442
|
+
comparisonPeriodLabel(period) {
|
|
8443
|
+
const key = period === 'current'
|
|
8444
|
+
? 'praxis.charts.runtime.comparisonCurrent'
|
|
8445
|
+
: 'praxis.charts.runtime.comparisonPrevious';
|
|
8446
|
+
const fallback = period === 'current' ? 'Current' : 'Previous';
|
|
8447
|
+
return this.i18n?.t(key, undefined, fallback, 'charts') ?? fallback;
|
|
8448
|
+
}
|
|
8449
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: AnalyticsChartConfigAdapterService, deps: [{ token: i1$1.PraxisI18nService, optional: true }], target: i0.ɵɵFactoryTarget.Injectable });
|
|
6556
8450
|
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: AnalyticsChartConfigAdapterService, providedIn: 'root' });
|
|
6557
8451
|
}
|
|
6558
8452
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: AnalyticsChartConfigAdapterService, decorators: [{
|
|
6559
8453
|
type: Injectable,
|
|
6560
8454
|
args: [{ providedIn: 'root' }]
|
|
6561
|
-
}]
|
|
8455
|
+
}], ctorParameters: () => [{ type: i1$1.PraxisI18nService, decorators: [{
|
|
8456
|
+
type: Optional
|
|
8457
|
+
}] }] });
|
|
6562
8458
|
|
|
6563
8459
|
class AnalyticsChartContractService {
|
|
6564
8460
|
analyticsSchema;
|
|
@@ -6661,7 +8557,7 @@ function normalizeError(error) {
|
|
|
6661
8557
|
|
|
6662
8558
|
const PRAXIS_CHART_BACKEND_MOCK_BAR = {
|
|
6663
8559
|
schemaMeta: {
|
|
6664
|
-
schemaId: 'api/human-resources/vw-perfil-heroi|post|response|
|
|
8560
|
+
schemaId: 'api/human-resources/vw-perfil-heroi|post|response|internal:false',
|
|
6665
8561
|
schemaHash: 'published-hash-hero-universe-v1',
|
|
6666
8562
|
resourcePath: 'api/human-resources/vw-perfil-heroi',
|
|
6667
8563
|
operation: 'post',
|
|
@@ -6691,7 +8587,7 @@ const PRAXIS_CHART_BACKEND_MOCK_BAR = {
|
|
|
6691
8587
|
sizing: { mode: 'fixed', height: 320 },
|
|
6692
8588
|
source: {
|
|
6693
8589
|
kind: 'praxis.stats',
|
|
6694
|
-
resource: 'api/human-resources/vw-perfil-heroi',
|
|
8590
|
+
resource: '/api/human-resources/vw-perfil-heroi',
|
|
6695
8591
|
operation: 'group-by',
|
|
6696
8592
|
options: {
|
|
6697
8593
|
orderBy: 'value-desc',
|
|
@@ -6743,7 +8639,7 @@ const PRAXIS_CHART_BACKEND_MOCK_BAR = {
|
|
|
6743
8639
|
};
|
|
6744
8640
|
const PRAXIS_CHART_BACKEND_MOCK_TIMESERIES = {
|
|
6745
8641
|
schemaMeta: {
|
|
6746
|
-
schemaId: 'api/human-resources/vw-indicadores-incidentes|post|response|
|
|
8642
|
+
schemaId: 'api/human-resources/vw-indicadores-incidentes|post|response|internal:false',
|
|
6747
8643
|
schemaHash: 'published-hash-incident-timeseries-v1',
|
|
6748
8644
|
resourcePath: 'api/human-resources/vw-indicadores-incidentes',
|
|
6749
8645
|
operation: 'post',
|
|
@@ -6767,7 +8663,7 @@ const PRAXIS_CHART_BACKEND_MOCK_TIMESERIES = {
|
|
|
6767
8663
|
subtitle: { key: 'charts.incidents.timeline.subtitle', fallback: 'Timeseries em /vw-indicadores-incidentes' },
|
|
6768
8664
|
source: {
|
|
6769
8665
|
kind: 'praxis.stats',
|
|
6770
|
-
resource: 'api/human-resources/vw-indicadores-incidentes',
|
|
8666
|
+
resource: '/api/human-resources/vw-indicadores-incidentes',
|
|
6771
8667
|
operation: 'timeseries',
|
|
6772
8668
|
options: {
|
|
6773
8669
|
granularity: 'month',
|
|
@@ -6799,7 +8695,7 @@ const PRAXIS_CHART_BACKEND_MOCK_TIMESERIES = {
|
|
|
6799
8695
|
};
|
|
6800
8696
|
const PRAXIS_CHART_BACKEND_MOCK_DONUT = {
|
|
6801
8697
|
schemaMeta: {
|
|
6802
|
-
schemaId: 'api/human-resources/vw-indicadores-incidentes|post|response|
|
|
8698
|
+
schemaId: 'api/human-resources/vw-indicadores-incidentes|post|response|internal:false',
|
|
6803
8699
|
schemaHash: 'published-hash-incident-severity-v1',
|
|
6804
8700
|
resourcePath: 'api/human-resources/vw-indicadores-incidentes',
|
|
6805
8701
|
operation: 'post',
|
|
@@ -6822,7 +8718,7 @@ const PRAXIS_CHART_BACKEND_MOCK_DONUT = {
|
|
|
6822
8718
|
title: { key: 'charts.incidents.severity.title', fallback: 'Severidade de incidentes' },
|
|
6823
8719
|
source: {
|
|
6824
8720
|
kind: 'praxis.stats',
|
|
6825
|
-
resource: 'api/human-resources/vw-indicadores-incidentes',
|
|
8721
|
+
resource: '/api/human-resources/vw-indicadores-incidentes',
|
|
6826
8722
|
operation: 'distribution',
|
|
6827
8723
|
options: {
|
|
6828
8724
|
mode: 'terms',
|
|
@@ -6855,7 +8751,7 @@ const PRAXIS_CHART_BACKEND_MOCK_DONUT = {
|
|
|
6855
8751
|
};
|
|
6856
8752
|
const PRAXIS_CHART_BACKEND_MOCK_HORIZONTAL_BAR = {
|
|
6857
8753
|
schemaMeta: {
|
|
6858
|
-
schemaId: 'api/human-resources/vw-analytics-folha-pagamento|post|response|
|
|
8754
|
+
schemaId: 'api/human-resources/vw-analytics-folha-pagamento|post|response|internal:false',
|
|
6859
8755
|
schemaHash: 'published-hash-payroll-department-ranking-v1',
|
|
6860
8756
|
resourcePath: 'api/human-resources/vw-analytics-folha-pagamento',
|
|
6861
8757
|
operation: 'post',
|
|
@@ -6881,7 +8777,7 @@ const PRAXIS_CHART_BACKEND_MOCK_HORIZONTAL_BAR = {
|
|
|
6881
8777
|
sizing: { mode: 'fixed', height: 340 },
|
|
6882
8778
|
source: {
|
|
6883
8779
|
kind: 'praxis.stats',
|
|
6884
|
-
resource: 'api/human-resources/vw-analytics-folha-pagamento',
|
|
8780
|
+
resource: '/api/human-resources/vw-analytics-folha-pagamento',
|
|
6885
8781
|
operation: 'group-by',
|
|
6886
8782
|
options: {
|
|
6887
8783
|
orderBy: 'value-desc',
|
|
@@ -6917,7 +8813,7 @@ const PRAXIS_CHART_BACKEND_MOCK_HORIZONTAL_BAR = {
|
|
|
6917
8813
|
};
|
|
6918
8814
|
const PRAXIS_CHART_BACKEND_MOCK_STACKED_AREA = {
|
|
6919
8815
|
schemaMeta: {
|
|
6920
|
-
schemaId: 'api/human-resources/vw-analytics-folha-pagamento|post|response|
|
|
8816
|
+
schemaId: 'api/human-resources/vw-analytics-folha-pagamento|post|response|internal:false',
|
|
6921
8817
|
schemaHash: 'published-hash-payroll-net-trend-stacked-v1',
|
|
6922
8818
|
resourcePath: 'api/human-resources/vw-analytics-folha-pagamento',
|
|
6923
8819
|
operation: 'post',
|
|
@@ -6942,7 +8838,7 @@ const PRAXIS_CHART_BACKEND_MOCK_STACKED_AREA = {
|
|
|
6942
8838
|
sizing: { mode: 'fixed', height: 340 },
|
|
6943
8839
|
source: {
|
|
6944
8840
|
kind: 'praxis.stats',
|
|
6945
|
-
resource: 'api/human-resources/vw-analytics-folha-pagamento',
|
|
8841
|
+
resource: '/api/human-resources/vw-analytics-folha-pagamento',
|
|
6946
8842
|
operation: 'timeseries',
|
|
6947
8843
|
options: {
|
|
6948
8844
|
granularity: 'month',
|
|
@@ -6973,7 +8869,7 @@ const PRAXIS_CHART_BACKEND_MOCK_STACKED_AREA = {
|
|
|
6973
8869
|
};
|
|
6974
8870
|
const PRAXIS_CHART_BACKEND_MOCK_MULTI_METRIC_BAR = {
|
|
6975
8871
|
schemaMeta: {
|
|
6976
|
-
schemaId: 'api/human-resources/vw-analytics-folha-pagamento|post|response|
|
|
8872
|
+
schemaId: 'api/human-resources/vw-analytics-folha-pagamento|post|response|internal:false',
|
|
6977
8873
|
schemaHash: 'published-hash-payroll-multi-metric-bar-v1',
|
|
6978
8874
|
resourcePath: 'api/human-resources/vw-analytics-folha-pagamento',
|
|
6979
8875
|
operation: 'post',
|
|
@@ -7001,7 +8897,7 @@ const PRAXIS_CHART_BACKEND_MOCK_MULTI_METRIC_BAR = {
|
|
|
7001
8897
|
sizing: { mode: 'fixed', height: 340 },
|
|
7002
8898
|
source: {
|
|
7003
8899
|
kind: 'praxis.stats',
|
|
7004
|
-
resource: 'api/human-resources/vw-analytics-folha-pagamento',
|
|
8900
|
+
resource: '/api/human-resources/vw-analytics-folha-pagamento',
|
|
7005
8901
|
operation: 'group-by',
|
|
7006
8902
|
options: {
|
|
7007
8903
|
orderBy: 'value-desc',
|
|
@@ -7039,7 +8935,7 @@ const PRAXIS_CHART_BACKEND_MOCK_MULTI_METRIC_BAR = {
|
|
|
7039
8935
|
};
|
|
7040
8936
|
const PRAXIS_CHART_BACKEND_MOCK_SCATTER = {
|
|
7041
8937
|
schemaMeta: {
|
|
7042
|
-
schemaId: 'api/human-resources/vw-analytics-folha-pagamento|post|response|
|
|
8938
|
+
schemaId: 'api/human-resources/vw-analytics-folha-pagamento|post|response|internal:false',
|
|
7043
8939
|
schemaHash: 'published-hash-payroll-scatter-v1',
|
|
7044
8940
|
resourcePath: 'api/human-resources/vw-analytics-folha-pagamento',
|
|
7045
8941
|
operation: 'post',
|
|
@@ -7064,7 +8960,7 @@ const PRAXIS_CHART_BACKEND_MOCK_SCATTER = {
|
|
|
7064
8960
|
sizing: { mode: 'fixed', height: 340 },
|
|
7065
8961
|
source: {
|
|
7066
8962
|
kind: 'praxis.stats',
|
|
7067
|
-
resource: 'api/human-resources/vw-analytics-folha-pagamento',
|
|
8963
|
+
resource: '/api/human-resources/vw-analytics-folha-pagamento',
|
|
7068
8964
|
operation: 'group-by',
|
|
7069
8965
|
options: {
|
|
7070
8966
|
orderBy: 'key-asc',
|
|
@@ -7095,7 +8991,7 @@ const PRAXIS_CHART_BACKEND_MOCK_SCATTER = {
|
|
|
7095
8991
|
};
|
|
7096
8992
|
const PRAXIS_CHART_BACKEND_MOCK_COMBO = {
|
|
7097
8993
|
schemaMeta: {
|
|
7098
|
-
schemaId: 'api/human-resources/vw-analytics-folha-pagamento|post|response|
|
|
8994
|
+
schemaId: 'api/human-resources/vw-analytics-folha-pagamento|post|response|internal:false',
|
|
7099
8995
|
schemaHash: 'published-hash-payroll-combo-v2',
|
|
7100
8996
|
resourcePath: 'api/human-resources/vw-analytics-folha-pagamento',
|
|
7101
8997
|
operation: 'post',
|
|
@@ -7120,7 +9016,7 @@ const PRAXIS_CHART_BACKEND_MOCK_COMBO = {
|
|
|
7120
9016
|
sizing: { mode: 'fixed', height: 340 },
|
|
7121
9017
|
source: {
|
|
7122
9018
|
kind: 'praxis.stats',
|
|
7123
|
-
resource: 'api/human-resources/vw-analytics-folha-pagamento',
|
|
9019
|
+
resource: '/api/human-resources/vw-analytics-folha-pagamento',
|
|
7124
9020
|
operation: 'timeseries',
|
|
7125
9021
|
options: {
|
|
7126
9022
|
granularity: 'month',
|
|
@@ -7603,7 +9499,7 @@ class PraxisChartCompositionShowcaseComponent {
|
|
|
7603
9499
|
};
|
|
7604
9500
|
}
|
|
7605
9501
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: PraxisChartCompositionShowcaseComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
7606
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.2.14", type: PraxisChartCompositionShowcaseComponent, isStandalone: true, selector: "praxis-chart-composition-showcase", inputs: { enableCustomization: { classPropertyName: "enableCustomization", publicName: "enableCustomization", isSignal: true, isRequired: false, transformFunction: null } },
|
|
9502
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.2.14", type: PraxisChartCompositionShowcaseComponent, isStandalone: true, selector: "praxis-chart-composition-showcase", inputs: { enableCustomization: { classPropertyName: "enableCustomization", publicName: "enableCustomization", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: `
|
|
7607
9503
|
<section class="showcase-shell">
|
|
7608
9504
|
<header class="showcase-hero">
|
|
7609
9505
|
<div class="showcase-copy">
|
|
@@ -7719,7 +9615,7 @@ class PraxisChartCompositionShowcaseComponent {
|
|
|
7719
9615
|
}
|
|
7720
9616
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: PraxisChartCompositionShowcaseComponent, decorators: [{
|
|
7721
9617
|
type: Component,
|
|
7722
|
-
args: [{ selector: 'praxis-chart-composition-showcase', standalone: true, imports: [DynamicWidgetPageComponent],
|
|
9618
|
+
args: [{ selector: 'praxis-chart-composition-showcase', standalone: true, imports: [DynamicWidgetPageComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: `
|
|
7723
9619
|
<section class="showcase-shell">
|
|
7724
9620
|
<header class="showcase-hero">
|
|
7725
9621
|
<div class="showcase-copy">
|
|
@@ -7834,11 +9730,65 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
|
|
|
7834
9730
|
`, styles: [":host{display:block}.showcase-shell{display:grid;gap:24px}.showcase-hero{display:grid;gap:20px;padding:28px;border-radius:28px;background:radial-gradient(circle at top right,rgba(18,99,180,.18),transparent 30%),linear-gradient(135deg,#071836f2,#1263b4c7);color:#f7fbff}.showcase-copy h2{margin:0 0 10px;font-size:clamp(1.8rem,3vw,2.6rem);line-height:1.05}.showcase-copy p{margin:0;max-width:52rem;color:#f7fbffe0}.showcase-eyebrow,.panel-kicker{margin:0 0 8px;font-size:.78rem;letter-spacing:.14em;text-transform:uppercase;color:#f7fbffb8}.showcase-controls{display:flex;flex-wrap:wrap;gap:16px}.control-group{display:grid;gap:8px}.control-group span{font-size:.84rem;color:#f7fbffc2}.control-buttons{display:flex;flex-wrap:wrap;gap:8px}.control-buttons button{border:1px solid rgba(247,251,255,.22);background:#f7fbff14;color:#f7fbff;border-radius:999px;padding:10px 14px;cursor:pointer;transition:background .16s ease,transform .16s ease}.control-buttons button.active{background:#f7fbff;color:#0d2d5f}.showcase-grid{display:grid;gap:20px;grid-template-columns:minmax(0,1.3fr) minmax(320px,.9fr)}.runtime-panel,.payload-panel{display:grid;gap:16px;padding:20px;border-radius:24px;border:1px solid color-mix(in srgb,var(--md-sys-color-outline, #c5c7ce) 76%,transparent);background:linear-gradient(180deg,#fffffff0,#f4f7fbf5)}.panel-header{display:flex;align-items:start;justify-content:space-between;gap:12px}.panel-header h3{margin:0;color:#142033}.panel-caption{margin:6px 0 0;color:#516074;font-size:.88rem;line-height:1.45}.panel-chip{border-radius:999px;padding:6px 10px;font-size:.76rem;background:#1263b41f;color:#1263b4}.payload-panel pre{margin:0;padding:16px;border-radius:18px;background:#09111f;color:#d7e6ff;overflow:auto;font-size:.84rem;line-height:1.5}@media(max-width:1080px){.showcase-grid{grid-template-columns:1fr}}\n"] }]
|
|
7835
9731
|
}], propDecorators: { enableCustomization: [{ type: i0.Input, args: [{ isSignal: true, alias: "enableCustomization", required: false }] }] } });
|
|
7836
9732
|
|
|
9733
|
+
const PRAXIS_CHART_AUTHORING_COVERAGE = [
|
|
9734
|
+
{ path: 'chartDocument', mode: 'advanced-structured', owner: 'chart-ai-manifest', rationale: 'Whole-document authoring is allowed for AI and structured import, but the visual editor must not become a raw JSON primary path.', proof: ['praxis-charts-authoring-manifest.spec.ts', 'praxis-chart-config-editor.spec.ts'] },
|
|
9735
|
+
{ path: 'chartDocument.version', mode: 'derived-readonly', owner: 'chart-contract-normalizer', rationale: 'Version is canonical schema governance and is normalized to the authorable x-ui.chart version.', proof: ['chart-authoring-coverage.matrix.spec.ts', 'praxis-charts-authoring-manifest.spec.ts'] },
|
|
9736
|
+
{ path: 'chartDocument.kind', mode: 'visual-editable', owner: 'chart-config-editor', rationale: 'Chart kind is a first-class visual decision with runtime mapper and AI manifest parity.', proof: ['praxis-chart-config-editor.spec.ts', 'praxis-charts-authoring-manifest.spec.ts'] },
|
|
9737
|
+
{ path: 'chartDocument.preset', mode: 'preserved-only', owner: 'chart-contract-normalizer', rationale: 'Preset is semantic starter intent; the current editor preserves it instead of presenting it as a misleading post-hoc visual control.', proof: ['praxis-chart-config-editor.spec.ts'] },
|
|
9738
|
+
{ path: 'chartDocument.chartId', mode: 'visual-editable', owner: 'chart-config-editor', rationale: 'Stable chart identity is editable for host/runtime diagnostics and event correlation.', proof: ['praxis-chart-config-editor.spec.ts'] },
|
|
9739
|
+
{ path: 'chartDocument.orientation', mode: 'ai-authorable', owner: 'chart-ai-manifest', rationale: 'Orientation is compiled by chart type and axis decisions; the visual editor currently preserves it without a separate control.', proof: ['praxis-charts-authoring-manifest.spec.ts', 'praxis-chart-config-editor.spec.ts'] },
|
|
9740
|
+
{ path: 'chartDocument.title', mode: 'visual-editable', owner: 'chart-config-editor', rationale: 'Title is primary presentation copy in the editor and runtime preview.', proof: ['praxis-chart-config-editor.spec.ts'] },
|
|
9741
|
+
{ path: 'chartDocument.subtitle', mode: 'visual-editable', owner: 'chart-config-editor', rationale: 'Subtitle is presentation copy edited alongside title.', proof: ['praxis-chart-config-editor.spec.ts'] },
|
|
9742
|
+
{ path: 'chartDocument.sizing', mode: 'visual-editable', owner: 'chart-config-editor', rationale: 'Sizing controls are canonical runtime layout decisions; deprecated height is normalized into sizing.height.', proof: ['praxis-chart-config-editor.spec.ts'] },
|
|
9743
|
+
{ path: 'chartDocument.height', mode: 'derived-readonly', owner: 'chart-contract-normalizer', rationale: 'Deprecated compatibility input is consumed and cleared in favor of chartDocument.sizing.height.', proof: ['praxis-chart-config-editor.spec.ts'] },
|
|
9744
|
+
{ path: 'chartDocument.source.kind', mode: 'visual-editable', owner: 'chart-config-editor', rationale: 'Source kind selects local derived data versus governed praxis.stats binding.', proof: ['praxis-chart-config-editor.spec.ts', 'praxis-chart-widget-config-editor.spec.ts'] },
|
|
9745
|
+
{ path: 'chartDocument.source.resource', mode: 'visual-editable', owner: 'chart-config-editor', rationale: 'Resource selection is governed by catalog options and fails closed when unavailable.', proof: ['praxis-chart-config-editor.spec.ts', 'praxis-charts-settings-panel.smoke.playwright.spec.ts'] },
|
|
9746
|
+
{ path: 'chartDocument.source.operation', mode: 'visual-editable', owner: 'chart-config-editor', rationale: 'Stats operation drives field compatibility, runtime request semantics and specialized options.', proof: ['praxis-chart-config-editor.spec.ts', 'praxis-charts-settings-panel.smoke.playwright.spec.ts'] },
|
|
9747
|
+
{ path: 'chartDocument.source.options.granularity', mode: 'visual-editable', owner: 'chart-config-editor', rationale: 'Timeseries granularity is visually edited with backend-supported values only.', proof: ['praxis-chart-config-editor.spec.ts', 'praxis-charts-settings-panel.smoke.playwright.spec.ts'] },
|
|
9748
|
+
{ path: 'chartDocument.source.options.fillGaps', mode: 'visual-editable', owner: 'chart-config-editor', rationale: 'Timeseries gap filling is a supported runtime option surfaced in the editor.', proof: ['praxis-chart-config-editor.spec.ts'] },
|
|
9749
|
+
{ path: 'chartDocument.source.options.mode', mode: 'visual-editable', owner: 'chart-config-editor', rationale: 'Distribution mode is visually edited for terms versus histogram materialization.', proof: ['praxis-chart-config-editor.spec.ts'] },
|
|
9750
|
+
{ path: 'chartDocument.source.options.bucketSize', mode: 'visual-editable', owner: 'chart-config-editor', rationale: 'Histogram bucket size is a supported distribution option.', proof: ['praxis-chart-config-editor.spec.ts'] },
|
|
9751
|
+
{ path: 'chartDocument.source.options.bucketCount', mode: 'visual-editable', owner: 'chart-config-editor', rationale: 'Histogram bucket count is a supported distribution option.', proof: ['praxis-chart-config-editor.spec.ts'] },
|
|
9752
|
+
{ path: 'chartDocument.source.options.orderBy', mode: 'preserved-only', owner: 'chart-contract-normalizer', rationale: 'Ordering is runtime-supported but not yet exposed as a primary visual control; preserving avoids silent loss.', proof: ['praxis-chart-config-editor.spec.ts'] },
|
|
9753
|
+
{ path: 'chartDocument.source.options.limit', mode: 'preserved-only', owner: 'chart-contract-normalizer', rationale: 'Source-level result limit remains governed by document/runtime semantics until a ranked visual control exists.', proof: ['praxis-chart-config-editor.spec.ts'] },
|
|
9754
|
+
{ path: 'chartDocument.source.refresh', mode: 'preserved-only', owner: 'chart-contract-normalizer', rationale: 'Refresh policy belongs to runtime/backend cadence governance; current editor preserves it.', proof: ['praxis-chart-config-editor.spec.ts'] },
|
|
9755
|
+
{ path: 'chartDocument.dimensions[]', mode: 'visual-editable', owner: 'chart-config-editor', rationale: 'Dimension field and role are visually editable with catalog compatibility checks.', proof: ['praxis-chart-config-editor.spec.ts', 'praxis-charts-authoring-manifest.spec.ts'] },
|
|
9756
|
+
{ path: 'chartDocument.dimensions[].label', mode: 'preserved-only', owner: 'chart-contract-normalizer', rationale: 'Dimension labels can be authored by AI/imported contracts and must survive visual edits until a governed label control exists.', proof: ['praxis-chart-config-editor.spec.ts'] },
|
|
9757
|
+
{ path: 'chartDocument.dimensions[].format', mode: 'preserved-only', owner: 'chart-contract-normalizer', rationale: 'Dimension formatting is runtime metadata and is preserved while not yet visually edited.', proof: ['praxis-chart-config-editor.spec.ts'] },
|
|
9758
|
+
{ path: 'chartDocument.metrics[]', mode: 'visual-editable', owner: 'chart-config-editor', rationale: 'Metric field, label, aggregation, axis and series kind are core visual analytics controls.', proof: ['praxis-chart-config-editor.spec.ts', 'praxis-charts-authoring-manifest.spec.ts'] },
|
|
9759
|
+
{ path: 'chartDocument.metrics[].color', mode: 'preserved-only', owner: 'chart-contract-normalizer', rationale: 'Series color is renderer-neutral config but not yet a governed swatch control in the editor.', proof: ['praxis-chart-config-editor.spec.ts'] },
|
|
9760
|
+
{ path: 'chartDocument.metrics[].format', mode: 'preserved-only', owner: 'chart-contract-normalizer', rationale: 'Metric formatting is preserved as runtime presentation metadata until visual format presets are introduced.', proof: ['praxis-chart-config-editor.spec.ts'] },
|
|
9761
|
+
{ path: 'chartDocument.aggregations[]', mode: 'ai-authorable', owner: 'chart-ai-manifest', rationale: 'Aggregation config is a structured analytics contract; visual authoring currently edits metric aggregation instead.', proof: ['chart-authoring-coverage.matrix.spec.ts', 'praxis-chart-config-editor.spec.ts'] },
|
|
9762
|
+
{ path: 'chartDocument.groupBy[]', mode: 'ai-authorable', owner: 'chart-ai-manifest', rationale: 'Grouping is derivable from dimensions for current visual flows and remains preserved for structured authoring.', proof: ['chart-authoring-coverage.matrix.spec.ts', 'praxis-chart-config-editor.spec.ts'] },
|
|
9763
|
+
{ path: 'chartDocument.sort[]', mode: 'preserved-only', owner: 'chart-contract-normalizer', rationale: 'Document-level sort is runtime-supported but lacks a governed visual ordering editor.', proof: ['praxis-chart-config-editor.spec.ts'] },
|
|
9764
|
+
{ path: 'chartDocument.filters[]', mode: 'preserved-only', owner: 'chart-contract-normalizer', rationale: 'Static document filters are preserved; dynamic authoring should prefer queryContext and backend capabilities.', proof: ['praxis-chart-config-editor.spec.ts'] },
|
|
9765
|
+
{ path: 'chartDocument.limit', mode: 'preserved-only', owner: 'chart-contract-normalizer', rationale: 'Document-level limit remains a structured runtime constraint until a governed visual control is added.', proof: ['praxis-chart-config-editor.spec.ts'] },
|
|
9766
|
+
{ path: 'chartDocument.legend', mode: 'visual-editable', owner: 'chart-config-editor', rationale: 'Legend visibility is a visual feature toggle and AI manifest operation.', proof: ['praxis-chart-config-editor.spec.ts', 'praxis-charts-authoring-manifest.spec.ts'] },
|
|
9767
|
+
{ path: 'chartDocument.labels', mode: 'visual-editable', owner: 'chart-config-editor', rationale: 'Data labels are a visual feature toggle in the appearance section.', proof: ['praxis-chart-config-editor.spec.ts'] },
|
|
9768
|
+
{ path: 'chartDocument.tooltip', mode: 'visual-editable', owner: 'chart-config-editor', rationale: 'Tooltip visibility is a visual feature toggle and AI manifest operation.', proof: ['praxis-chart-config-editor.spec.ts', 'praxis-charts-authoring-manifest.spec.ts'] },
|
|
9769
|
+
{ path: 'chartDocument.theme.palette', mode: 'visual-editable', owner: 'chart-config-editor', rationale: 'Palette token/custom values are visually edited and preserved as renderer-neutral theme contract.', proof: ['praxis-chart-config-editor.spec.ts'] },
|
|
9770
|
+
{ path: 'chartDocument.theme.variant', mode: 'visual-editable', owner: 'chart-config-editor', rationale: 'Theme variant is a visual editor control and runtime presentation decision.', proof: ['praxis-chart-config-editor.spec.ts', 'praxis-charts-settings-panel.smoke.playwright.spec.ts'] },
|
|
9771
|
+
{ path: 'chartDocument.theme.surface.mode', mode: 'visual-editable', owner: 'chart-config-editor', rationale: 'Surface mode is visually edited to align chart containment with host context.', proof: ['praxis-chart-config-editor.spec.ts'] },
|
|
9772
|
+
{ path: 'chartDocument.theme.surface.background', mode: 'preserved-only', owner: 'chart-contract-normalizer', rationale: 'Surface color details are contract-supported but not yet governed by tokenized visual controls.', proof: ['praxis-chart-config-editor.spec.ts'] },
|
|
9773
|
+
{ path: 'chartDocument.theme.surface.borderColor', mode: 'preserved-only', owner: 'chart-contract-normalizer', rationale: 'Surface border color is preserved until a token-aware border editor exists.', proof: ['praxis-chart-config-editor.spec.ts'] },
|
|
9774
|
+
{ path: 'chartDocument.theme.surface.borderWidth', mode: 'preserved-only', owner: 'chart-contract-normalizer', rationale: 'Surface border width is preserved to avoid dropping host-authored surface detail.', proof: ['praxis-chart-config-editor.spec.ts'] },
|
|
9775
|
+
{ path: 'chartDocument.theme.surface.borderRadius', mode: 'preserved-only', owner: 'chart-contract-normalizer', rationale: 'Surface border radius is preserved to avoid dropping host-authored surface detail.', proof: ['praxis-chart-config-editor.spec.ts'] },
|
|
9776
|
+
{ path: 'chartDocument.state.empty', mode: 'visual-editable', owner: 'chart-config-editor', rationale: 'Empty-state title/description are visible runtime presentation copy.', proof: ['praxis-chart-config-editor.spec.ts'] },
|
|
9777
|
+
{ path: 'chartDocument.state.loading', mode: 'visual-editable', owner: 'chart-config-editor', rationale: 'Loading-state title/description are visible runtime presentation copy.', proof: ['praxis-chart-config-editor.spec.ts'] },
|
|
9778
|
+
{ path: 'chartDocument.state.error', mode: 'visual-editable', owner: 'chart-config-editor', rationale: 'Error-state title/description are visible runtime presentation copy.', proof: ['praxis-chart-config-editor.spec.ts'] },
|
|
9779
|
+
{ path: 'chartDocument.events.pointClick', mode: 'visual-editable', owner: 'chart-config-editor', rationale: 'Point click action is configured visually while raw point evidence remains a separate output.', proof: ['praxis-chart-config-editor.spec.ts', 'praxis-charts-authoring-manifest.spec.ts'] },
|
|
9780
|
+
{ path: 'chartDocument.events.selectionChange', mode: 'visual-editable', owner: 'chart-config-editor', rationale: 'Selection change supports the governed single-selection event contract.', proof: ['praxis-chart-config-editor.spec.ts', 'praxis-charts-authoring-manifest.spec.ts'] },
|
|
9781
|
+
{ path: 'chartDocument.events.drillDown', mode: 'visual-editable', owner: 'chart-config-editor', rationale: 'Drilldown target and mapping are visually authorable with governed target catalogs.', proof: ['praxis-chart-config-editor.spec.ts', 'praxis-charts-authoring-manifest.spec.ts'] },
|
|
9782
|
+
{ path: 'chartDocument.events.crossFilter', mode: 'visual-editable', owner: 'chart-config-editor', rationale: 'Cross-filter target and mapping are visually authorable with governed target catalogs.', proof: ['praxis-chart-config-editor.spec.ts', 'praxis-charts-authoring-manifest.spec.ts'] },
|
|
9783
|
+
{ path: 'chartDocument.motion', mode: 'visual-editable', owner: 'chart-config-editor', rationale: 'Motion enablement and preset are visual runtime behavior controls.', proof: ['praxis-chart-config-editor.spec.ts'] },
|
|
9784
|
+
{ path: 'queryContext', mode: 'advanced-structured', owner: 'chart-widget-config-editor', rationale: 'Query context is a structured widget runtime input edited as an advanced JSON fallback and validated as object-only.', proof: ['praxis-chart-widget-config-editor.spec.ts', 'praxis-charts-authoring-manifest.spec.ts'] },
|
|
9785
|
+
];
|
|
9786
|
+
|
|
7837
9787
|
const chartDocumentSchema = {
|
|
7838
9788
|
type: 'object',
|
|
7839
9789
|
required: ['version', 'kind', 'source'],
|
|
7840
9790
|
properties: {
|
|
7841
|
-
version: { const:
|
|
9791
|
+
version: { const: PRAXIS_X_UI_CHART_AUTHORABLE_VERSION },
|
|
7842
9792
|
kind: { enum: ['bar', 'combo', 'horizontal-bar', 'line', 'pie', 'donut', 'area', 'stacked-bar', 'stacked-area', 'scatter'] },
|
|
7843
9793
|
chartId: { type: 'string' },
|
|
7844
9794
|
title: { oneOf: [{ type: 'string' }, { type: 'object' }] },
|
|
@@ -7867,21 +9817,22 @@ const PRAXIS_CHARTS_AUTHORING_MANIFEST = {
|
|
|
7867
9817
|
runtimeInputs: [
|
|
7868
9818
|
{ name: 'chartDocument', type: 'PraxisXUiChartContract | null', description: 'Canonical x-ui.chart document used as the authoring source of truth.' },
|
|
7869
9819
|
{ name: 'config', type: 'PraxisChartConfig', description: 'Runtime chart configuration mapped from or supplied beside the canonical chart document.' },
|
|
7870
|
-
{ name: 'queryContext', type: 'PraxisChartQueryContext | null', description: '
|
|
9820
|
+
{ name: 'queryContext', type: 'PraxisChartQueryContext | null', description: 'Canonical @praxisui/core query context observed by remote datasource requests; flat filters execute in praxis.stats and governed filterExpression is preserved but rejected until stats expression support is proven.' },
|
|
7871
9821
|
{ name: 'remoteDataResolver', type: 'PraxisChartRemoteDataResolver | null', description: 'Host-governed remote row resolver for integrations outside the default praxis.stats client.' },
|
|
7872
|
-
{ name: 'availableResources', type: 'ChartEditorResourceOption[]', description: '
|
|
7873
|
-
{ name: 'availableFields', type: 'ChartEditorFieldOption[]', description: '
|
|
7874
|
-
{ 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.' },
|
|
7875
9825
|
],
|
|
7876
9826
|
editableTargets: [
|
|
7877
9827
|
{ kind: 'chartType', resolver: 'x-ui-chart-kind', description: 'Canonical `kind` field in PraxisXUiChartContract.' },
|
|
7878
9828
|
{ kind: 'series', resolver: 'x-ui-chart-metric-by-field', description: 'Metric-backed series definition in `metrics[]`, keyed by field.' },
|
|
7879
9829
|
{ kind: 'axis', resolver: 'x-ui-chart-dimension-or-metric-axis', description: 'Dimension role, metric axis and chart orientation semantics.' },
|
|
7880
9830
|
{ kind: 'dataBinding', resolver: 'x-ui-chart-source-and-field-catalog', description: 'Source, dimensions, metrics and resource/field bindings.' },
|
|
7881
|
-
{ kind: 'queryContext', resolver: 'praxis-chart-query-context', description: 'Runtime queryContext contract for filters, sort, limit and
|
|
9831
|
+
{ kind: 'queryContext', resolver: 'praxis-chart-query-context', description: 'Runtime @praxisui/core queryContext contract for filters, filterExpression, sort, limit, page and governance metadata.' },
|
|
9832
|
+
{ kind: 'pointClick', resolver: 'x-ui-chart-events-point-click', description: 'Raw pointClick evidence plus a separate pointAction output for configured pointClick actions.' },
|
|
7882
9833
|
{ kind: 'crossFilter', resolver: 'x-ui-chart-events-cross-filter', description: 'Cross-filter event mapping emitted as structured query-context filters.' },
|
|
7883
9834
|
{ kind: 'drilldown', resolver: 'x-ui-chart-events-drill-down', description: 'Drilldown event mapping and governed target.' },
|
|
7884
|
-
{ kind: 'selection', resolver: 'x-ui-chart-events-selection-change', description: '
|
|
9835
|
+
{ kind: 'selection', resolver: 'x-ui-chart-events-selection-change', description: 'Single-point selection event enablement and mapping semantics; toggle and multi-select are intentionally not advertised.' },
|
|
7885
9836
|
{ kind: 'legend', resolver: 'x-ui-chart-legend-feature', description: 'Legend visibility through boolean or toggleable feature object.' },
|
|
7886
9837
|
{ kind: 'tooltip', resolver: 'x-ui-chart-tooltip-feature', description: 'Tooltip visibility through boolean or toggleable feature object.' },
|
|
7887
9838
|
],
|
|
@@ -8021,7 +9972,7 @@ const PRAXIS_CHARTS_AUTHORING_MANIFEST = {
|
|
|
8021
9972
|
required: ['sourceKind'],
|
|
8022
9973
|
properties: {
|
|
8023
9974
|
sourceKind: { enum: ['praxis.stats', 'derived'] },
|
|
8024
|
-
resource: { type: 'string' },
|
|
9975
|
+
resource: { type: 'string', pattern: '^/(?!/)' },
|
|
8025
9976
|
operation: { enum: ['group-by', 'timeseries', 'distribution'] },
|
|
8026
9977
|
dimensions: { type: 'array', items: { type: 'object', required: ['field'], properties: { field: { type: 'string' }, role: { type: 'string' } } } },
|
|
8027
9978
|
metrics: { type: 'array', items: { type: 'object', required: ['field'], properties: { field: { type: 'string' }, label: { oneOf: [{ type: 'string' }, { type: 'object' }] }, aggregation: { enum: ['sum', 'avg', 'min', 'max', 'count', 'distinct-count'] }, seriesKind: { enum: ['bar', 'line', 'area'] }, axis: { enum: ['primary', 'secondary'] }, color: { type: 'string' }, format: { type: 'string' }, afterField: { type: 'string' } } } },
|
|
@@ -8030,10 +9981,10 @@ const PRAXIS_CHARTS_AUTHORING_MANIFEST = {
|
|
|
8030
9981
|
effects: [{ kind: 'compile-domain-patch', handler: 'chart-data-resource-bind', handlerContract: {
|
|
8031
9982
|
reads: ['availableResources[]', 'availableFields[]', 'chartDocument.source', 'chartDocument.dimensions[]', 'chartDocument.metrics[]'],
|
|
8032
9983
|
writes: ['chartDocument.source', 'chartDocument.dimensions[]', 'chartDocument.metrics[]'],
|
|
8033
|
-
identityKeys: ['availableResources[].path', '
|
|
8034
|
-
inputSchema: { type: 'object', required: ['sourceKind'], properties: { sourceKind: { enum: ['praxis.stats', 'derived'] }, resource: { type: 'string' }, operation: { enum: ['group-by', 'timeseries', 'distribution'] }, dimensions: { type: 'array' }, metrics: { type: 'array' } } },
|
|
9984
|
+
identityKeys: ['availableResources[].path', 'availableFields[].field'],
|
|
9985
|
+
inputSchema: { type: 'object', required: ['sourceKind'], properties: { sourceKind: { enum: ['praxis.stats', 'derived'] }, resource: { type: 'string', pattern: '^/(?!/)' }, operation: { enum: ['group-by', 'timeseries', 'distribution'] }, dimensions: { type: 'array' }, metrics: { type: 'array' } } },
|
|
8035
9986
|
failureModes: ['resource-not-in-api-metadata', 'field-not-in-schema', 'stats-operation-not-supported', 'remote-binding-incomplete'],
|
|
8036
|
-
description: 'Binds a derived or praxis.stats source using the
|
|
9987
|
+
description: 'Binds a derived or praxis.stats source using governed catalogs. For praxis.stats, availableResources[].id is discovery-only: validation requires the matching availableResources[].path and compilation persists that path as chartDocument.source.resource.',
|
|
8037
9988
|
} }],
|
|
8038
9989
|
destructive: false,
|
|
8039
9990
|
requiresConfirmation: false,
|
|
@@ -8048,15 +9999,37 @@ const PRAXIS_CHARTS_AUTHORING_MANIFEST = {
|
|
|
8048
9999
|
scope: 'dataBinding',
|
|
8049
10000
|
targetKind: 'queryContext',
|
|
8050
10001
|
target: { kind: 'queryContext', resolver: 'praxis-chart-query-context', ambiguityPolicy: 'fail', required: false },
|
|
8051
|
-
inputSchema: { type: 'object', properties: { filters: { type: 'object' }, sort: { type: 'array', items: { type: 'string' } }, limit: { type: 'number' }, page: { type: 'object' }, meta: { type: 'object' } } },
|
|
10002
|
+
inputSchema: { type: 'object', properties: { filters: { type: 'object' }, filterExpression: { type: 'object' }, sort: { type: 'array', items: { type: 'string' } }, limit: { type: 'number' }, page: { type: 'object' }, meta: { type: 'object' } } },
|
|
8052
10003
|
effects: [{ kind: 'set-value', path: 'queryContext' }],
|
|
8053
10004
|
destructive: false,
|
|
8054
10005
|
requiresConfirmation: false,
|
|
8055
|
-
validators: ['query-context-structured', 'query-context-fields-exist', 'query-context-safe-values', 'editor-runtime-round-trip'],
|
|
10006
|
+
validators: ['query-context-structured', 'query-context-filter-expression-stats-unsupported', 'query-context-fields-exist', 'query-context-safe-values', 'editor-runtime-round-trip'],
|
|
8056
10007
|
affectedPaths: ['queryContext'],
|
|
8057
10008
|
submissionImpact: 'affects-schema-backed-data',
|
|
8058
10009
|
preconditions: ['config-initialized'],
|
|
8059
10010
|
},
|
|
10011
|
+
{
|
|
10012
|
+
operationId: 'pointClick.configure',
|
|
10013
|
+
title: 'Configure chart point click',
|
|
10014
|
+
scope: 'eventMapping',
|
|
10015
|
+
targetKind: 'pointClick',
|
|
10016
|
+
target: { kind: 'pointClick', resolver: 'x-ui-chart-events-point-click', ambiguityPolicy: 'fail', required: false },
|
|
10017
|
+
inputSchema: { type: 'object', required: ['action'], properties: { action: { enum: ['filter-widget', 'open-detail', 'navigate', 'update-context', 'emit'] }, target: { type: 'string' }, mapping: { type: 'object', additionalProperties: { type: 'string' } } } },
|
|
10018
|
+
effects: [{ kind: 'compile-domain-patch', handler: 'chart-event-point-click-configure', handlerContract: {
|
|
10019
|
+
reads: ['chartDocument.events.pointClick', 'chartDocument.dimensions[]', 'availableTargets[]'],
|
|
10020
|
+
writes: ['chartDocument.events.pointClick'],
|
|
10021
|
+
identityKeys: ['availableTargets[].id'],
|
|
10022
|
+
inputSchema: { type: 'object', required: ['action'], properties: { action: { enum: ['filter-widget', 'open-detail', 'navigate', 'update-context', 'emit'] }, target: { type: 'string' }, mapping: { type: 'object', additionalProperties: { type: 'string' } } } },
|
|
10023
|
+
failureModes: ['target-not-in-catalog', 'mapping-source-field-missing', 'mapping-target-invalid', 'unsafe-event-action'],
|
|
10024
|
+
description: 'Configures pointClick through the pointAction output while keeping pointClick as raw point evidence.',
|
|
10025
|
+
} }],
|
|
10026
|
+
destructive: false,
|
|
10027
|
+
requiresConfirmation: false,
|
|
10028
|
+
validators: ['point-click-action-structured', 'event-target-governed', 'event-mapping-fields-exist', 'event-action-supported', 'editor-runtime-round-trip'],
|
|
10029
|
+
affectedPaths: ['chartDocument.events.pointClick'],
|
|
10030
|
+
submissionImpact: 'affects-schema-backed-data',
|
|
10031
|
+
preconditions: ['config-initialized'],
|
|
10032
|
+
},
|
|
8060
10033
|
{
|
|
8061
10034
|
operationId: 'crossFilter.configure',
|
|
8062
10035
|
title: 'Configure chart cross-filter',
|
|
@@ -8070,7 +10043,7 @@ const PRAXIS_CHARTS_AUTHORING_MANIFEST = {
|
|
|
8070
10043
|
identityKeys: ['availableTargets[].id'],
|
|
8071
10044
|
inputSchema: { type: 'object', required: ['action'], properties: { action: { enum: ['filter-widget', 'open-detail', 'navigate', 'update-context', 'emit'] }, target: { type: 'string' }, mapping: { type: 'object', additionalProperties: { type: 'string' } } } },
|
|
8072
10045
|
failureModes: ['target-not-in-catalog', 'mapping-source-field-missing', 'mapping-target-invalid', 'unsafe-event-action'],
|
|
8073
|
-
description: 'Configures crossFilter as a structured event action
|
|
10046
|
+
description: 'Configures crossFilter as a structured event action emitted through the crossFilter output with safe query-context filters to a governed target. For praxis.stats rows, key is governed bucket identity and label is presentation evidence; mappings may use key when the target schema accepts that identity.',
|
|
8074
10047
|
} }],
|
|
8075
10048
|
destructive: false,
|
|
8076
10049
|
requiresConfirmation: false,
|
|
@@ -8092,7 +10065,7 @@ const PRAXIS_CHARTS_AUTHORING_MANIFEST = {
|
|
|
8092
10065
|
identityKeys: ['availableTargets[].id'],
|
|
8093
10066
|
inputSchema: { type: 'object', required: ['action'], properties: { action: { enum: ['filter-widget', 'open-detail', 'navigate', 'update-context', 'emit'] }, target: { type: 'string' }, mapping: { type: 'object', additionalProperties: { type: 'string' } } } },
|
|
8094
10067
|
failureModes: ['target-not-in-catalog', 'mapping-source-field-missing', 'drilldown-target-invalid', 'unsafe-event-action'],
|
|
8095
|
-
description: 'Configures drillDown through governed route/widget targets and structured field mappings.',
|
|
10068
|
+
description: 'Configures drillDown through the dedicated drillDown output with governed route/widget targets and structured field mappings.',
|
|
8096
10069
|
} }],
|
|
8097
10070
|
destructive: false,
|
|
8098
10071
|
requiresConfirmation: false,
|
|
@@ -8114,11 +10087,11 @@ const PRAXIS_CHARTS_AUTHORING_MANIFEST = {
|
|
|
8114
10087
|
identityKeys: ['availableTargets[].id'],
|
|
8115
10088
|
inputSchema: { type: 'object', required: ['action'], properties: { action: { enum: ['filter-widget', 'open-detail', 'navigate', 'update-context', 'emit'] }, target: { type: 'string' }, mapping: { type: 'object', additionalProperties: { type: 'string' } } } },
|
|
8116
10089
|
failureModes: ['target-not-in-catalog', 'mapping-source-field-missing', 'mapping-target-invalid', 'unsafe-event-action'],
|
|
8117
|
-
description: 'Configures selectionChange as
|
|
10090
|
+
description: 'Configures selectionChange as single-point selection evidence whose event key is governed by the operation.',
|
|
8118
10091
|
} }],
|
|
8119
10092
|
destructive: false,
|
|
8120
10093
|
requiresConfirmation: false,
|
|
8121
|
-
validators: ['selection-output-structured', 'event-mapping-fields-exist', 'event-action-supported', 'editor-runtime-round-trip'],
|
|
10094
|
+
validators: ['selection-output-structured', 'event-target-governed', 'event-mapping-fields-exist', 'event-action-supported', 'editor-runtime-round-trip'],
|
|
8122
10095
|
affectedPaths: ['chartDocument.events.selectionChange'],
|
|
8123
10096
|
submissionImpact: 'affects-schema-backed-data',
|
|
8124
10097
|
preconditions: ['config-initialized'],
|
|
@@ -8156,7 +10129,7 @@ const PRAXIS_CHARTS_AUTHORING_MANIFEST = {
|
|
|
8156
10129
|
],
|
|
8157
10130
|
validators: [
|
|
8158
10131
|
{ validatorId: 'chart-document-shape', level: 'error', code: 'CHART_DOCUMENT_SHAPE', description: 'Document must conform to PraxisXUiChartContract.' },
|
|
8159
|
-
{ validatorId: 'chart-version-supported', level: 'error', code: 'CHART_VERSION_SUPPORTED', description:
|
|
10132
|
+
{ validatorId: 'chart-version-supported', level: 'error', code: 'CHART_VERSION_SUPPORTED', description: `Only x-ui.chart version ${PRAXIS_X_UI_CHART_AUTHORABLE_VERSION} is authorable.` },
|
|
8160
10133
|
{ validatorId: 'chart-type-supported', level: 'error', code: 'CHART_TYPE_SUPPORTED', description: 'Chart kind must be supported by @praxisui/charts.' },
|
|
8161
10134
|
{ validatorId: 'chart-type-series-axis-compatible', level: 'error', code: 'CHART_TYPE_SERIES_AXIS_COMPATIBLE', description: 'Chart kind, dimensions, metrics and axes must be compatible.' },
|
|
8162
10135
|
{ validatorId: 'pie-single-metric', level: 'error', code: 'PIE_SINGLE_METRIC', description: 'Pie and donut charts support a single metric.' },
|
|
@@ -8170,15 +10143,17 @@ const PRAXIS_CHARTS_AUTHORING_MANIFEST = {
|
|
|
8170
10143
|
{ validatorId: 'axis-field-exists', level: 'error', code: 'AXIS_FIELD_EXISTS', description: 'Axis/dimension fields must exist in availableFields or schema context.' },
|
|
8171
10144
|
{ validatorId: 'secondary-axis-combo-only', level: 'error', code: 'SECONDARY_AXIS_COMBO_ONLY', description: 'Secondary axis is supported only for combo charts.' },
|
|
8172
10145
|
{ validatorId: 'cartesian-dimension-required', level: 'error', code: 'CARTESIAN_DIMENSION_REQUIRED', description: 'Cartesian charts require at least one dimension.' },
|
|
8173
|
-
{ validatorId: 'remote-resource-in-api-metadata', level: 'error', code: 'REMOTE_RESOURCE_IN_API_METADATA', description: 'Remote praxis.stats resource must
|
|
8174
|
-
{ 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.' },
|
|
8175
10148
|
{ validatorId: 'stats-operation-supported', level: 'error', code: 'STATS_OPERATION_SUPPORTED', description: 'Remote datasource operation must be supported by the stats backend.' },
|
|
8176
|
-
{ validatorId: 'query-context-structured', level: 'error', code: 'QUERY_CONTEXT_STRUCTURED', description: 'queryContext must use
|
|
10149
|
+
{ validatorId: 'query-context-structured', level: 'error', code: 'QUERY_CONTEXT_STRUCTURED', description: 'queryContext must use the canonical @praxisui/core structure for filters, filterExpression, sort, limit, page and meta.' },
|
|
10150
|
+
{ validatorId: 'query-context-filter-expression-stats-unsupported', level: 'error', code: 'QUERY_CONTEXT_FILTER_EXPRESSION_STATS_UNSUPPORTED', description: 'praxis.stats currently executes only flat queryContext.filters; governed filterExpression must be preserved for diagnostics and rejected until backend expression support is declared.' },
|
|
8177
10151
|
{ validatorId: 'query-context-fields-exist', level: 'warning', code: 'QUERY_CONTEXT_FIELDS_EXIST', description: 'queryContext filter fields should exist in the schema/data context.' },
|
|
8178
10152
|
{ validatorId: 'query-context-safe-values', level: 'error', code: 'QUERY_CONTEXT_SAFE_VALUES', description: 'queryContext values must be serializable safe data.' },
|
|
10153
|
+
{ validatorId: 'point-click-action-structured', level: 'error', code: 'POINT_CLICK_ACTION_STRUCTURED', description: 'Configured pointClick actions must be emitted through pointAction while pointClick remains raw point evidence.' },
|
|
8179
10154
|
{ validatorId: 'cross-filter-output-structured', level: 'error', code: 'CROSS_FILTER_OUTPUT_STRUCTURED', description: 'Cross-filter output must be structured as query-context filters.' },
|
|
8180
|
-
{ validatorId: 'event-target-governed', level: 'error', code: 'EVENT_TARGET_GOVERNED', description: 'Event targets must
|
|
8181
|
-
{ validatorId: 'event-mapping-fields-exist', level: 'error', code: 'EVENT_MAPPING_FIELDS_EXIST', description: 'Event mapping source fields must exist in chart data context.' },
|
|
10155
|
+
{ validatorId: 'event-target-governed', level: 'error', code: 'EVENT_TARGET_GOVERNED', description: 'Event targets must resolve to exactly one availableTargets[].id and declare the authored event plus action; missing, duplicate or cross-event identities fail closed.' },
|
|
10156
|
+
{ validatorId: 'event-mapping-fields-exist', level: 'error', code: 'EVENT_MAPPING_FIELDS_EXIST', description: 'Event mapping source fields must exist in chart data context; praxis.stats reserves key for bucket identity and label for presentation.' },
|
|
8182
10157
|
{ validatorId: 'event-action-supported', level: 'error', code: 'EVENT_ACTION_SUPPORTED', description: 'Event action must be one of the supported structured action kinds.' },
|
|
8183
10158
|
{ validatorId: 'drilldown-target-governed', level: 'error', code: 'DRILLDOWN_TARGET_GOVERNED', description: 'Drilldown target must be a governed route or widget target.' },
|
|
8184
10159
|
{ validatorId: 'selection-output-structured', level: 'error', code: 'SELECTION_OUTPUT_STRUCTURED', description: 'Selection output must resolve selected point filters deterministically.' },
|
|
@@ -8188,8 +10163,11 @@ const PRAXIS_CHARTS_AUTHORING_MANIFEST = {
|
|
|
8188
10163
|
roundTripRequirements: [
|
|
8189
10164
|
'The canonical saved shape is PraxisXUiChartContract, not raw ECharts options.',
|
|
8190
10165
|
'PraxisChartConfigEditor must preserve chart kind, source, dimensions, metrics, events, legend and tooltip across apply/save/reset/reopen.',
|
|
8191
|
-
'Remote bindings
|
|
10166
|
+
'Remote bindings persist availableResources[].path; availableResources[].id remains discovery identity, and transient resource/field/target catalogs and diagnostics are never persisted in widget inputs.',
|
|
8192
10167
|
'Cross-filter, drilldown and selection authoring must persist structured event actions, not prompt examples or command strings.',
|
|
10168
|
+
'[P1] Backend handler, resolver and validators now execute directly; end-to-end Page Builder parity remains open until assistant requests project the transient availableTargets[].events catalog through validationContext and expose its fail-closed diagnostics.',
|
|
10169
|
+
'[P2] Transform-output semantics and target input-schema/port fields must be materialized and validated by Core; local transform.output inspection and backend-enriched inputFields are not substitutes for those canonical projections.',
|
|
10170
|
+
'Enrichment of capability-backed availableFields with /schemas/filtered property metadata remains a later schema-flow gate.',
|
|
8193
10171
|
],
|
|
8194
10172
|
examples: [
|
|
8195
10173
|
{ id: 'set-bar-chart', request: 'Use a bar chart for employees by department.', operationId: 'chart.type.set', params: { kind: 'bar' }, isPositive: true },
|
|
@@ -8199,11 +10177,12 @@ const PRAXIS_CHARTS_AUTHORING_MANIFEST = {
|
|
|
8199
10177
|
{ id: 'bind-stats-resource', request: 'Bind this chart to payroll stats grouped by department.', operationId: 'data.resource.bind', params: { sourceKind: 'praxis.stats', resource: '/api/stats/payroll', operation: 'group-by' }, isPositive: true },
|
|
8200
10178
|
{ id: 'set-query-context', request: 'Limit the chart to active employees and sort by total descending.', operationId: 'queryContext.set', params: { filters: { status: 'ACTIVE' }, sort: ['total,desc'], limit: 10 }, isPositive: true },
|
|
8201
10179
|
{ id: 'configure-cross-filter', request: 'Use selected department to filter the employee table.', operationId: 'crossFilter.configure', params: { action: 'filter-widget', target: 'employeesTable', mapping: { department: 'department' } }, isPositive: true },
|
|
10180
|
+
{ id: 'configure-stats-key-cross-filter', request: 'Use the governed department bucket identity to filter the employee table.', operationId: 'crossFilter.configure', params: { action: 'filter-widget', target: 'employeesTable', mapping: { key: 'departmentIdsIn' } }, isPositive: true },
|
|
8202
10181
|
{ id: 'configure-drilldown', request: 'Open the department detail when a point is clicked.', operationId: 'drilldown.configure', params: { action: 'navigate', target: '/departments/detail', mapping: { department: 'departmentId' } }, isPositive: true },
|
|
8203
10182
|
{ id: 'configure-selection', request: 'Emit selected department as a structured selection event.', operationId: 'selection.configure', params: { action: 'emit', mapping: { department: 'department' } }, isPositive: true },
|
|
8204
10183
|
{ id: 'toggle-legend', request: 'Show the legend.', operationId: 'legend.configure', params: { enabled: true }, isPositive: true },
|
|
8205
10184
|
{ id: 'toggle-tooltip', request: 'Disable tooltips.', operationId: 'tooltip.configure', params: { enabled: false }, isPositive: true },
|
|
8206
|
-
{ id: 'round-trip-editor-runtime', request: 'Save this chart in the editor and reopen it without changing the runtime chart document.', operationId: 'chart.document.set', params: { version:
|
|
10185
|
+
{ id: 'round-trip-editor-runtime', request: 'Save this chart in the editor and reopen it without changing the runtime chart document.', operationId: 'chart.document.set', params: { version: PRAXIS_X_UI_CHART_AUTHORABLE_VERSION, kind: 'bar', source: { kind: 'derived' }, dimensions: [{ field: 'department' }], metrics: [{ field: 'total', aggregation: 'count' }] }, isPositive: true },
|
|
8207
10186
|
{ id: 'reject-ambiguous-series-target', request: 'Remove total when more than one chart series resolves to total.', operationId: 'series.remove', target: 'total', params: { field: 'total' }, isPositive: false },
|
|
8208
10187
|
{ id: 'reject-prompt-routing', request: 'Use the prompt example text to decide which backend endpoint to call.', operationId: 'data.resource.bind', params: { sourceKind: 'praxis.stats', resource: 'from prompt example' }, isPositive: false },
|
|
8209
10188
|
{ id: 'reject-unsafe-target', request: 'Navigate to javascript:alert(1) on drilldown.', operationId: 'drilldown.configure', params: { event: 'drillDown', action: 'navigate', target: 'javascript:alert(1)' }, isPositive: false },
|
|
@@ -8218,4 +10197,4 @@ const PRAXIS_CHARTS_AUTHORING_MANIFEST = {
|
|
|
8218
10197
|
* Generated bundle index. Do not edit.
|
|
8219
10198
|
*/
|
|
8220
10199
|
|
|
8221
|
-
export { AnalyticsChartConfigAdapterService, AnalyticsChartContractService, ChartContractNormalizerService, ChartContractValidationService, ChartEditorDefaultsService, ChartEditorPreviewMapperService, PRAXIS_CHARTS_AUTHORING_MANIFEST, PRAXIS_CHARTS_I18N, PRAXIS_CHART_BACKEND_MOCK_BAR, PRAXIS_CHART_BACKEND_MOCK_COMBO, PRAXIS_CHART_BACKEND_MOCK_DONUT, PRAXIS_CHART_BACKEND_MOCK_HORIZONTAL_BAR, PRAXIS_CHART_BACKEND_MOCK_MULTI_METRIC_BAR, PRAXIS_CHART_BACKEND_MOCK_SCATTER, PRAXIS_CHART_BACKEND_MOCK_STACKED_AREA, PRAXIS_CHART_BACKEND_MOCK_TIMESERIES, PRAXIS_CHART_COMPONENT_METADATA, PRAXIS_CHART_DRILLDOWN_DATA_BY_MONTH, PRAXIS_CHART_DRILLDOWN_PANEL_METADATA, PRAXIS_CHART_ENGINE, PRAXIS_CHART_ENGINE_FACTORY, PRAXIS_CHART_PALETTE_TOKENS, PRAXIS_CHART_STATE_PROBE_COMPONENT_METADATA, PRAXIS_CHART_THEME_VARIANTS, PraxisChartBackendPayloadAdapterService, PraxisChartCanonicalContractMapperService, PraxisChartComponent, PraxisChartCompositionShowcaseComponent, PraxisChartConfigEditor, PraxisChartDataTransformerService, PraxisChartDrilldownPanelComponent,
|
|
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 };
|