@praxisui/charts 10.0.0-rc.38 → 10.0.0-rc.39
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/ai/component-registry.json +90 -19
- package/fesm2022/praxisui-charts.mjs +1855 -142
- package/package.json +3 -3
- package/types/praxisui-charts.d.ts +97 -11
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
2
|
import { Injectable, Optional, Inject, InjectionToken, input, booleanAttribute, output, viewChild, inject, ElementRef, NgZone, DestroyRef, signal, linkedSignal, computed, afterNextRender, effect, untracked, ChangeDetectionStrategy, Component, LOCALE_ID, Injector, ENVIRONMENT_INITIALIZER, ViewChild, Input } from '@angular/core';
|
|
3
3
|
import * as i1 from '@praxisui/core';
|
|
4
|
-
import { buildApiUrl, API_URL, PraxisI18nService, ComponentMetadataRegistry, SETTINGS_PANEL_BRIDGE, normalizePraxisDataQueryContext, resolvePraxisFilterCriteria, BUILTIN_PAGE_THEME_PRESETS, PraxisIconButtonComponent, normalizePraxisPresentationVisualization, resolvePraxisMicroVisualizationTone, AnalyticsStatsRequestBuilderService, AnalyticsPresentationResolver, ResourceDiscoveryService, humanizeCatalogFieldName, createDefaultTableConfig, providePraxisI18n, PRAXIS_TABLE_DETAIL_INLINE_RENDERERS, DynamicWidgetPageComponent, SETTINGS_PANEL_DATA, inferDomainSemanticTone, inferDomainSemanticHexColor, PraxisIconDirective } from '@praxisui/core';
|
|
4
|
+
import { validateResourceQuickViewBinding, normalizeResourceQuickViewBinding, buildApiUrl, API_URL, PraxisI18nService, ComponentMetadataRegistry, SETTINGS_PANEL_BRIDGE, ResourceQuickViewService, normalizePraxisDataQueryContext, resolvePraxisFilterCriteria, BUILTIN_PAGE_THEME_PRESETS, PraxisIconButtonComponent, PraxisQuickViewTriggerDirective, normalizePraxisPresentationVisualization, resolvePraxisMicroVisualizationTone, AnalyticsStatsRequestBuilderService, AnalyticsPresentationResolver, ResourceDiscoveryService, humanizeCatalogFieldName, createDefaultTableConfig, providePraxisI18n, PRAXIS_TABLE_DETAIL_INLINE_RENDERERS, DynamicWidgetPageComponent, SETTINGS_PANEL_DATA, inferDomainSemanticTone, inferDomainSemanticHexColor, PraxisIconDirective } from '@praxisui/core';
|
|
5
5
|
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
|
6
6
|
import { throwError, map, finalize, shareReplay, of, isObservable, from, firstValueFrom, timeout, BehaviorSubject, Subscription } from 'rxjs';
|
|
7
7
|
import * as i1$2 from '@angular/material/tooltip';
|
|
@@ -11,8 +11,8 @@ import { HttpErrorResponse } from '@angular/common/http';
|
|
|
11
11
|
import { catchError } from 'rxjs/operators';
|
|
12
12
|
import { AnalyticsTableConfigAdapterService, AnalyticsTableStatsApiService, ANALYTICS_TABLE_ROW_KEY_FIELD, PraxisTable } from '@praxisui/table';
|
|
13
13
|
import { color, use, init } from 'echarts/core';
|
|
14
|
-
import { BarChart, FunnelChart, GaugeChart, LineChart, PieChart, ScatterChart, TreemapChart } from 'echarts/charts';
|
|
15
|
-
import { AriaComponent, DatasetComponent, GridComponent, LegendComponent, TimelineComponent, TitleComponent, TooltipComponent, TransformComponent } from 'echarts/components';
|
|
14
|
+
import { BarChart, FunnelChart, GaugeChart, HeatmapChart, LineChart, PieChart, ScatterChart, TreemapChart } from 'echarts/charts';
|
|
15
|
+
import { AriaComponent, DatasetComponent, GridComponent, LegendComponent, TimelineComponent, TitleComponent, TooltipComponent, TransformComponent, VisualMapComponent } from 'echarts/components';
|
|
16
16
|
import { UniversalTransition } from 'echarts/features';
|
|
17
17
|
import { CanvasRenderer } from 'echarts/renderers';
|
|
18
18
|
import * as i1$3 from '@angular/forms';
|
|
@@ -79,6 +79,108 @@ const PRAXIS_CHART_THEME_VARIANTS = {
|
|
|
79
79
|
},
|
|
80
80
|
};
|
|
81
81
|
|
|
82
|
+
function hasChartQuickViewBinding(config) {
|
|
83
|
+
if (config.interactions?.quickView) {
|
|
84
|
+
if (validateResourceQuickViewBinding(config.interactions.quickView).valid) {
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return (config.series ?? []).some((series) => {
|
|
89
|
+
if (!series.quickView) {
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
return validateResourceQuickViewBinding(series.quickView).valid;
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
function resolveChartSeriesForPoint(config, seriesId, seriesName) {
|
|
96
|
+
const seriesList = config.series ?? [];
|
|
97
|
+
if (!seriesList.length) {
|
|
98
|
+
return undefined;
|
|
99
|
+
}
|
|
100
|
+
if (seriesId) {
|
|
101
|
+
const exact = seriesList.find((candidate) => candidate.id === seriesId);
|
|
102
|
+
if (exact) {
|
|
103
|
+
return exact;
|
|
104
|
+
}
|
|
105
|
+
const prefixed = seriesList.find((candidate) => seriesId.startsWith(`${candidate.id}:`));
|
|
106
|
+
if (prefixed) {
|
|
107
|
+
return prefixed;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
if (seriesName) {
|
|
111
|
+
const byName = seriesList.find((candidate) => candidate.name === seriesName);
|
|
112
|
+
if (byName) {
|
|
113
|
+
return byName;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return seriesList[0];
|
|
117
|
+
}
|
|
118
|
+
function resolveChartPointQuickView(config, point) {
|
|
119
|
+
const series = resolveChartSeriesForPoint(config, point.seriesId, point.seriesName);
|
|
120
|
+
const rawBinding = series?.quickView ?? config.interactions?.quickView;
|
|
121
|
+
if (!rawBinding) {
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
const validation = validateResourceQuickViewBinding(rawBinding);
|
|
125
|
+
if (!validation.valid || !validation.binding) {
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
const binding = validation.binding;
|
|
129
|
+
if (!point.data || typeof point.data !== 'object' || Array.isArray(point.data)) {
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
const record = {
|
|
133
|
+
...point.data,
|
|
134
|
+
};
|
|
135
|
+
const sourceIdentityField = binding.recordOpen.sourceIdentityField;
|
|
136
|
+
const isCanonicalStats = config.dataSource?.kind === 'remote'
|
|
137
|
+
&& config.dataSource.query?.sourceKind === 'praxis.stats';
|
|
138
|
+
if (isCanonicalStats) {
|
|
139
|
+
const statsOperation = config.dataSource?.kind === 'remote'
|
|
140
|
+
? config.dataSource.query?.statsOperation
|
|
141
|
+
: undefined;
|
|
142
|
+
const distributionMode = config.dataSource?.kind === 'remote'
|
|
143
|
+
? config.dataSource.query?.distributionMode
|
|
144
|
+
: undefined;
|
|
145
|
+
if (statsOperation === 'timeseries'
|
|
146
|
+
|| (statsOperation === 'distribution'
|
|
147
|
+
&& (distributionMode === 'histogram' || record['from'] != null || record['to'] != null))) {
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
const categoryField = config.axes?.x?.field
|
|
151
|
+
?? config.series.find((candidate) => candidate.categoryField)?.categoryField;
|
|
152
|
+
const secondaryCategoryField = config.series.find((candidate) => candidate.secondaryCategoryField)?.secondaryCategoryField
|
|
153
|
+
?? (config.axes?.y?.type === 'category' ? config.axes.y.field : undefined);
|
|
154
|
+
const coordinates = record['coordinates'] && typeof record['coordinates'] === 'object' && !Array.isArray(record['coordinates'])
|
|
155
|
+
? record['coordinates']
|
|
156
|
+
: null;
|
|
157
|
+
if (sourceIdentityField === categoryField && record['key'] !== undefined && record['key'] !== null) {
|
|
158
|
+
record[sourceIdentityField] = record['key'];
|
|
159
|
+
}
|
|
160
|
+
else if (sourceIdentityField === secondaryCategoryField
|
|
161
|
+
&& record['secondaryKey'] !== undefined
|
|
162
|
+
&& record['secondaryKey'] !== null) {
|
|
163
|
+
record[sourceIdentityField] = record['secondaryKey'];
|
|
164
|
+
}
|
|
165
|
+
else if (coordinates && sourceIdentityField in coordinates) {
|
|
166
|
+
record[sourceIdentityField] = coordinates[sourceIdentityField];
|
|
167
|
+
}
|
|
168
|
+
else if (sourceIdentityField !== 'key' && sourceIdentityField !== 'secondaryKey') {
|
|
169
|
+
delete record[sourceIdentityField];
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
const identityCandidate = record[sourceIdentityField];
|
|
173
|
+
const hasValidIdentity = (typeof identityCandidate === 'number' && Number.isFinite(identityCandidate))
|
|
174
|
+
|| (typeof identityCandidate === 'string' && identityCandidate.trim().length > 0);
|
|
175
|
+
if (!hasValidIdentity) {
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
178
|
+
return {
|
|
179
|
+
binding,
|
|
180
|
+
record,
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
82
184
|
class PraxisChartDataTransformerService {
|
|
83
185
|
transform(config, rows) {
|
|
84
186
|
if (config.type === 'pie' || config.type === 'donut') {
|
|
@@ -93,6 +195,9 @@ class PraxisChartDataTransformerService {
|
|
|
93
195
|
if (config.type === 'gauge') {
|
|
94
196
|
return this.transformGauge(config, rows);
|
|
95
197
|
}
|
|
198
|
+
if (config.type === 'heatmap') {
|
|
199
|
+
return this.transformHeatmap(config, rows);
|
|
200
|
+
}
|
|
96
201
|
if (config.type === 'scatter') {
|
|
97
202
|
return this.transformScatter(config, rows);
|
|
98
203
|
}
|
|
@@ -151,6 +256,99 @@ class PraxisChartDataTransformerService {
|
|
|
151
256
|
diagnostic: { code, message },
|
|
152
257
|
};
|
|
153
258
|
}
|
|
259
|
+
transformHeatmap(config, rows) {
|
|
260
|
+
const seriesConfig = config.series[0];
|
|
261
|
+
const xCategoryField = seriesConfig?.categoryField ?? config.axes?.x?.field;
|
|
262
|
+
const yCategoryField = xCategoryField
|
|
263
|
+
? (this.resolveSecondaryCategoryField(config, xCategoryField)
|
|
264
|
+
?? (config.axes?.y?.type === 'category' && config.axes?.y?.field !== xCategoryField
|
|
265
|
+
? config.axes.y.field
|
|
266
|
+
: undefined))
|
|
267
|
+
: undefined;
|
|
268
|
+
if (!seriesConfig || !xCategoryField || !yCategoryField || !rows.length) {
|
|
269
|
+
return {
|
|
270
|
+
mode: 'heatmap',
|
|
271
|
+
categories: [],
|
|
272
|
+
series: [],
|
|
273
|
+
slices: [],
|
|
274
|
+
hasData: false,
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
const usesCanonicalStatsIdentity = config.dataSource?.kind === 'remote'
|
|
278
|
+
&& config.dataSource.query?.sourceKind === 'praxis.stats';
|
|
279
|
+
const xBuckets = this.buildCategoryBuckets(config, rows, xCategoryField);
|
|
280
|
+
const yBuckets = this.buildSecondaryCategoryBuckets(rows, yCategoryField, usesCanonicalStatsIdentity);
|
|
281
|
+
const xCategories = xBuckets.map((bucket) => bucket.label);
|
|
282
|
+
const yCategories = yBuckets.map((bucket) => bucket.label);
|
|
283
|
+
const cells = [];
|
|
284
|
+
let min = Number.POSITIVE_INFINITY;
|
|
285
|
+
let max = Number.NEGATIVE_INFINITY;
|
|
286
|
+
let hasFiniteValue = false;
|
|
287
|
+
xBuckets.forEach((xBucket, xIndex) => {
|
|
288
|
+
yBuckets.forEach((yBucket, yIndex) => {
|
|
289
|
+
const cellRows = xBucket.rows.filter((row) => this.resolveSecondaryCategoryIdentity(row, this.normalizeCategory(row[yCategoryField]), usesCanonicalStatsIdentity) === yBucket.identity);
|
|
290
|
+
if (!cellRows.length) {
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
const metricField = seriesConfig.metric?.field;
|
|
294
|
+
const aggregation = seriesConfig.metric?.aggregation ?? 'sum';
|
|
295
|
+
const allCellMetricValuesMissing = Boolean(metricField
|
|
296
|
+
&& aggregation !== 'count'
|
|
297
|
+
&& aggregation !== 'distinct-count'
|
|
298
|
+
&& cellRows.every((row) => this.extractStrictMetricValue(row, seriesConfig) === null));
|
|
299
|
+
const rawValue = allCellMetricValuesMissing
|
|
300
|
+
? null
|
|
301
|
+
: this.aggregate(cellRows, seriesConfig);
|
|
302
|
+
if (rawValue !== null && Number.isFinite(rawValue)) {
|
|
303
|
+
hasFiniteValue = true;
|
|
304
|
+
if (rawValue < min) {
|
|
305
|
+
min = rawValue;
|
|
306
|
+
}
|
|
307
|
+
if (rawValue > max) {
|
|
308
|
+
max = rawValue;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
const source = cellRows[0];
|
|
312
|
+
const pointTuple = [xIndex, yIndex, rawValue];
|
|
313
|
+
const cellData = this.buildBucketPointData(cellRows, pointTuple, config, seriesConfig, {
|
|
314
|
+
category: xBucket.label,
|
|
315
|
+
secondaryCategory: yBucket.label,
|
|
316
|
+
});
|
|
317
|
+
cells.push({
|
|
318
|
+
id: `${seriesConfig.id}:${xBucket.identity}:${yBucket.identity}`,
|
|
319
|
+
xIndex,
|
|
320
|
+
yIndex,
|
|
321
|
+
xCategory: xBucket.label,
|
|
322
|
+
yCategory: yBucket.label,
|
|
323
|
+
value: pointTuple,
|
|
324
|
+
rawValue,
|
|
325
|
+
data: cellData,
|
|
326
|
+
});
|
|
327
|
+
});
|
|
328
|
+
});
|
|
329
|
+
const normalizedMin = hasFiniteValue ? min : 0;
|
|
330
|
+
const normalizedMax = hasFiniteValue
|
|
331
|
+
? (max > min ? max : min + (min === 0 ? 1 : Math.abs(min) * 0.25))
|
|
332
|
+
: 1;
|
|
333
|
+
return {
|
|
334
|
+
mode: 'heatmap',
|
|
335
|
+
categories: xCategories,
|
|
336
|
+
series: [],
|
|
337
|
+
slices: [],
|
|
338
|
+
heatmap: {
|
|
339
|
+
id: seriesConfig.id,
|
|
340
|
+
name: seriesConfig.name ?? seriesConfig.metric?.label ?? seriesConfig.id,
|
|
341
|
+
xCategories,
|
|
342
|
+
yCategories,
|
|
343
|
+
cells,
|
|
344
|
+
min: normalizedMin,
|
|
345
|
+
max: normalizedMax,
|
|
346
|
+
labelsVisible: seriesConfig.labels?.visible ?? false,
|
|
347
|
+
color: seriesConfig.color,
|
|
348
|
+
},
|
|
349
|
+
hasData: xCategories.length > 0 && yCategories.length > 0 && hasFiniteValue,
|
|
350
|
+
};
|
|
351
|
+
}
|
|
154
352
|
transformCartesian(config, rows) {
|
|
155
353
|
const categoryField = config.axes?.x?.field;
|
|
156
354
|
if (!categoryField || !rows.length || !config.series.length) {
|
|
@@ -165,7 +363,12 @@ class PraxisChartDataTransformerService {
|
|
|
165
363
|
const categoryBuckets = this.buildCategoryBuckets(config, rows, categoryField);
|
|
166
364
|
const categories = categoryBuckets.map((bucket) => bucket.label);
|
|
167
365
|
const timeAxis = this.usesCanonicalTimeCoordinates(config);
|
|
168
|
-
const
|
|
366
|
+
const secondaryCategoryField = this.resolveSecondaryCategoryField(config, categoryField);
|
|
367
|
+
const hasSecondaryValues = Boolean(secondaryCategoryField
|
|
368
|
+
&& rows.some((row) => row[secondaryCategoryField] !== undefined && row[secondaryCategoryField] !== null));
|
|
369
|
+
const series = hasSecondaryValues && secondaryCategoryField
|
|
370
|
+
? this.buildPivotedCartesianSeries(config, rows, categoryBuckets, secondaryCategoryField, timeAxis)
|
|
371
|
+
: config.series.map((seriesConfig) => this.buildCartesianSeries(config, seriesConfig, categoryBuckets, timeAxis));
|
|
169
372
|
return {
|
|
170
373
|
mode: 'cartesian',
|
|
171
374
|
categories,
|
|
@@ -194,7 +397,7 @@ class PraxisChartDataTransformerService {
|
|
|
194
397
|
name: bucket.label,
|
|
195
398
|
value,
|
|
196
399
|
color: seriesConfig.color,
|
|
197
|
-
data: this.
|
|
400
|
+
data: this.buildBucketPointData(bucket.rows, value, config, seriesConfig),
|
|
198
401
|
}];
|
|
199
402
|
});
|
|
200
403
|
return {
|
|
@@ -220,11 +423,85 @@ class PraxisChartDataTransformerService {
|
|
|
220
423
|
const value = this.aggregate(bucket.rows, seriesConfig);
|
|
221
424
|
const source = bucket.rows[0];
|
|
222
425
|
return source
|
|
223
|
-
? this.
|
|
426
|
+
? this.buildBucketPointData(bucket.rows, timeAxis ? [bucket.coordinate, value] : value, config, seriesConfig)
|
|
224
427
|
: (timeAxis ? [bucket.coordinate, value] : value);
|
|
225
428
|
}),
|
|
226
429
|
};
|
|
227
430
|
}
|
|
431
|
+
resolveSecondaryCategoryField(config, primaryCategoryField) {
|
|
432
|
+
const seriesSecondary = config.series.find((s) => Boolean(s.secondaryCategoryField?.trim()))?.secondaryCategoryField?.trim();
|
|
433
|
+
if (seriesSecondary && seriesSecondary !== primaryCategoryField) {
|
|
434
|
+
return seriesSecondary;
|
|
435
|
+
}
|
|
436
|
+
if (config.dataSource?.kind === 'remote') {
|
|
437
|
+
const secondDimension = config.dataSource.query?.dimensions?.[1]?.trim();
|
|
438
|
+
if (secondDimension && secondDimension !== primaryCategoryField) {
|
|
439
|
+
return secondDimension;
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
return undefined;
|
|
443
|
+
}
|
|
444
|
+
buildPivotedCartesianSeries(config, rows, categoryBuckets, secondaryCategoryField, timeAxis) {
|
|
445
|
+
const usesCanonicalStatsIdentity = config.dataSource?.kind === 'remote'
|
|
446
|
+
&& config.dataSource.query?.sourceKind === 'praxis.stats';
|
|
447
|
+
const secondaryBuckets = this.buildSecondaryCategoryBuckets(rows, secondaryCategoryField, usesCanonicalStatsIdentity);
|
|
448
|
+
const multiMetric = config.series.length > 1;
|
|
449
|
+
return config.series.flatMap((seriesConfig) => secondaryBuckets.map((secondaryBucket) => {
|
|
450
|
+
const baseName = seriesConfig.name ?? seriesConfig.metric?.label ?? seriesConfig.id;
|
|
451
|
+
return {
|
|
452
|
+
id: `${seriesConfig.id}:${secondaryBucket.identity}`,
|
|
453
|
+
name: multiMetric ? `${baseName} - ${secondaryBucket.label}` : secondaryBucket.label,
|
|
454
|
+
type: this.resolveCartesianSeriesType(config, seriesConfig),
|
|
455
|
+
axis: seriesConfig.axis ?? 'primary',
|
|
456
|
+
stack: config.type === 'stacked-bar' || config.type === 'stacked-area'
|
|
457
|
+
? seriesConfig.stackId ?? 'stack-1'
|
|
458
|
+
: undefined,
|
|
459
|
+
smooth: seriesConfig.smooth ?? this.isAreaLikeSeries(config, seriesConfig),
|
|
460
|
+
area: this.isAreaLikeSeries(config, seriesConfig),
|
|
461
|
+
color: multiMetric ? undefined : undefined,
|
|
462
|
+
labelsVisible: seriesConfig.labels?.visible ?? false,
|
|
463
|
+
points: categoryBuckets.map((primaryBucket) => {
|
|
464
|
+
const cellRows = primaryBucket.rows.filter((row) => this.resolveSecondaryCategoryIdentity(row, this.normalizeCategory(row[secondaryCategoryField]), usesCanonicalStatsIdentity) === secondaryBucket.identity);
|
|
465
|
+
if (!cellRows.length) {
|
|
466
|
+
return timeAxis ? [primaryBucket.coordinate, null] : null;
|
|
467
|
+
}
|
|
468
|
+
const value = this.aggregate(cellRows, seriesConfig);
|
|
469
|
+
return this.buildBucketPointData(cellRows, timeAxis ? [primaryBucket.coordinate, value] : value, config, seriesConfig);
|
|
470
|
+
}),
|
|
471
|
+
};
|
|
472
|
+
}));
|
|
473
|
+
}
|
|
474
|
+
buildSecondaryCategoryBuckets(rows, secondaryCategoryField, usesCanonicalStatsIdentity) {
|
|
475
|
+
const buckets = new Map();
|
|
476
|
+
for (const row of rows) {
|
|
477
|
+
if (row[secondaryCategoryField] === undefined) {
|
|
478
|
+
continue;
|
|
479
|
+
}
|
|
480
|
+
const label = this.normalizeCategory(row['secondaryLabel'] ?? row[secondaryCategoryField]);
|
|
481
|
+
const identity = this.resolveSecondaryCategoryIdentity(row, label, usesCanonicalStatsIdentity);
|
|
482
|
+
const existing = buckets.get(identity);
|
|
483
|
+
if (existing) {
|
|
484
|
+
existing.rows.push(row);
|
|
485
|
+
continue;
|
|
486
|
+
}
|
|
487
|
+
buckets.set(identity, {
|
|
488
|
+
identity,
|
|
489
|
+
label,
|
|
490
|
+
coordinate: label,
|
|
491
|
+
rows: [row],
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
return Array.from(buckets.values());
|
|
495
|
+
}
|
|
496
|
+
resolveSecondaryCategoryIdentity(row, label, usesCanonicalStatsIdentity) {
|
|
497
|
+
if (!usesCanonicalStatsIdentity) {
|
|
498
|
+
return label;
|
|
499
|
+
}
|
|
500
|
+
if (row['secondaryKey'] !== null && row['secondaryKey'] !== undefined) {
|
|
501
|
+
return `key:${this.typedIdentity(row['secondaryKey'])}`;
|
|
502
|
+
}
|
|
503
|
+
return `label:${label}`;
|
|
504
|
+
}
|
|
228
505
|
buildCategoryBuckets(config, rows, categoryField) {
|
|
229
506
|
const buckets = new Map();
|
|
230
507
|
const usesCanonicalStatsIdentity = config.dataSource?.kind === 'remote'
|
|
@@ -310,6 +587,59 @@ class PraxisChartDataTransformerService {
|
|
|
310
587
|
value,
|
|
311
588
|
};
|
|
312
589
|
}
|
|
590
|
+
buildBucketPointData(rows, value, config, seriesConfig, extra) {
|
|
591
|
+
const source = rows[0] ?? {};
|
|
592
|
+
const pointData = {
|
|
593
|
+
...source,
|
|
594
|
+
...(extra ?? {}),
|
|
595
|
+
value,
|
|
596
|
+
};
|
|
597
|
+
if (rows.length > 1) {
|
|
598
|
+
const identityFields = this.collectQuickViewIdentityFields(config, seriesConfig);
|
|
599
|
+
for (const field of identityFields) {
|
|
600
|
+
if (!this.hasUniformEntityIdentity(rows, field)) {
|
|
601
|
+
delete pointData[field];
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
return pointData;
|
|
606
|
+
}
|
|
607
|
+
collectQuickViewIdentityFields(config, seriesConfig) {
|
|
608
|
+
const fields = new Set();
|
|
609
|
+
const seriesField = seriesConfig?.quickView?.recordOpen?.sourceIdentityField?.trim();
|
|
610
|
+
if (seriesField) {
|
|
611
|
+
fields.add(seriesField);
|
|
612
|
+
}
|
|
613
|
+
const chartField = config.interactions?.quickView?.recordOpen?.sourceIdentityField?.trim();
|
|
614
|
+
if (chartField) {
|
|
615
|
+
fields.add(chartField);
|
|
616
|
+
}
|
|
617
|
+
for (const candidate of config.series ?? []) {
|
|
618
|
+
const candidateField = candidate.quickView?.recordOpen?.sourceIdentityField?.trim();
|
|
619
|
+
if (candidateField) {
|
|
620
|
+
fields.add(candidateField);
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
return Array.from(fields);
|
|
624
|
+
}
|
|
625
|
+
hasUniformEntityIdentity(rows, field) {
|
|
626
|
+
if (!rows.length) {
|
|
627
|
+
return false;
|
|
628
|
+
}
|
|
629
|
+
const first = rows[0]?.[field];
|
|
630
|
+
if (typeof first !== 'string' && typeof first !== 'number') {
|
|
631
|
+
return false;
|
|
632
|
+
}
|
|
633
|
+
const normalizedFirst = String(first).trim();
|
|
634
|
+
if (!normalizedFirst) {
|
|
635
|
+
return false;
|
|
636
|
+
}
|
|
637
|
+
return rows.every((row) => {
|
|
638
|
+
const candidate = row?.[field];
|
|
639
|
+
return (typeof candidate === 'string' || typeof candidate === 'number')
|
|
640
|
+
&& String(candidate).trim() === normalizedFirst;
|
|
641
|
+
});
|
|
642
|
+
}
|
|
313
643
|
hasPointValue(point) {
|
|
314
644
|
if (point == null) {
|
|
315
645
|
return false;
|
|
@@ -383,9 +713,12 @@ class PraxisChartDataTransformerService {
|
|
|
383
713
|
};
|
|
384
714
|
}
|
|
385
715
|
aggregate(rows, seriesConfig) {
|
|
716
|
+
const field = seriesConfig.metric?.field;
|
|
717
|
+
if (field && rows.length > 0 && rows.every((row) => row[field] === null)) {
|
|
718
|
+
return null;
|
|
719
|
+
}
|
|
386
720
|
const aggregation = seriesConfig.metric?.aggregation ?? 'sum';
|
|
387
721
|
if (aggregation === 'count') {
|
|
388
|
-
const field = seriesConfig.metric?.field;
|
|
389
722
|
if (field) {
|
|
390
723
|
const values = rows
|
|
391
724
|
.map((row) => this.extractMetricValue(row, seriesConfig))
|
|
@@ -397,19 +730,20 @@ class PraxisChartDataTransformerService {
|
|
|
397
730
|
return rows.length;
|
|
398
731
|
}
|
|
399
732
|
if (aggregation === 'distinct-count') {
|
|
400
|
-
const field = seriesConfig.metric?.field;
|
|
401
733
|
if (!field) {
|
|
402
734
|
return 0;
|
|
403
735
|
}
|
|
404
736
|
return new Set(rows.map((row) => this.normalizeCategory(row[field]))).size;
|
|
405
737
|
}
|
|
406
738
|
const values = rows
|
|
739
|
+
.filter((row) => !field || row[field] !== null)
|
|
407
740
|
.map((row) => this.extractMetricValue(row, seriesConfig))
|
|
408
741
|
.filter((value) => Number.isFinite(value));
|
|
409
742
|
if (!values.length)
|
|
410
743
|
return 0;
|
|
411
744
|
switch (aggregation) {
|
|
412
745
|
case 'avg':
|
|
746
|
+
case 'avg-per-cycle-occurrence':
|
|
413
747
|
return values.reduce((sum, value) => sum + value, 0) / values.length;
|
|
414
748
|
case 'min':
|
|
415
749
|
return Math.min(...values);
|
|
@@ -497,8 +831,37 @@ class ChartContractNormalizerService {
|
|
|
497
831
|
document = this.normalizeKindSpecificFields(document);
|
|
498
832
|
document = this.normalizeOperationSpecificFields(document);
|
|
499
833
|
document = this.normalizeSourceSpecificFields(document);
|
|
834
|
+
document = this.normalizeQuickViewBindings(document);
|
|
500
835
|
return document;
|
|
501
836
|
}
|
|
837
|
+
normalizeQuickViewBindings(document) {
|
|
838
|
+
let nextDocument = document;
|
|
839
|
+
if (nextDocument.metrics?.length) {
|
|
840
|
+
nextDocument = {
|
|
841
|
+
...nextDocument,
|
|
842
|
+
metrics: nextDocument.metrics.map((metric) => {
|
|
843
|
+
if (!metric.quickView) {
|
|
844
|
+
return metric;
|
|
845
|
+
}
|
|
846
|
+
const normalized = normalizeResourceQuickViewBinding(metric.quickView);
|
|
847
|
+
return normalized ? { ...metric, quickView: normalized } : metric;
|
|
848
|
+
}),
|
|
849
|
+
};
|
|
850
|
+
}
|
|
851
|
+
if (nextDocument.events?.quickView) {
|
|
852
|
+
const normalized = normalizeResourceQuickViewBinding(nextDocument.events.quickView);
|
|
853
|
+
if (normalized) {
|
|
854
|
+
nextDocument = {
|
|
855
|
+
...nextDocument,
|
|
856
|
+
events: {
|
|
857
|
+
...nextDocument.events,
|
|
858
|
+
quickView: normalized,
|
|
859
|
+
},
|
|
860
|
+
};
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
return nextDocument;
|
|
864
|
+
}
|
|
502
865
|
normalizeMotion(document) {
|
|
503
866
|
if (!document.motion) {
|
|
504
867
|
return document;
|
|
@@ -595,8 +958,18 @@ class ChartContractNormalizerService {
|
|
|
595
958
|
const options = { ...document.source.options };
|
|
596
959
|
if (document.source.operation !== 'timeseries') {
|
|
597
960
|
options.granularity = undefined;
|
|
961
|
+
}
|
|
962
|
+
if (document.source.operation !== 'timeseries' && document.source.operation !== 'group-by') {
|
|
598
963
|
options.fillGaps = undefined;
|
|
599
964
|
}
|
|
965
|
+
if (document.source.operation !== 'group-by') {
|
|
966
|
+
if (options.timezone !== undefined)
|
|
967
|
+
options.timezone = undefined;
|
|
968
|
+
if (options.periodField !== undefined)
|
|
969
|
+
options.periodField = undefined;
|
|
970
|
+
if (options.periodPreset !== undefined)
|
|
971
|
+
options.periodPreset = undefined;
|
|
972
|
+
}
|
|
600
973
|
if (document.source.operation !== 'distribution') {
|
|
601
974
|
options.mode = undefined;
|
|
602
975
|
options.bucketSize = undefined;
|
|
@@ -632,7 +1005,8 @@ class ChartContractNormalizerService {
|
|
|
632
1005
|
|| document.kind === 'donut'
|
|
633
1006
|
|| document.kind === 'funnel'
|
|
634
1007
|
|| document.kind === 'pyramid'
|
|
635
|
-
|| document.kind === 'treemap'
|
|
1008
|
+
|| document.kind === 'treemap'
|
|
1009
|
+
|| document.kind === 'heatmap';
|
|
636
1010
|
}
|
|
637
1011
|
normalizeCssSizeValue(value) {
|
|
638
1012
|
if (typeof value !== 'string') {
|
|
@@ -665,6 +1039,30 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
|
|
|
665
1039
|
args: [{ providedIn: 'root' }]
|
|
666
1040
|
}] });
|
|
667
1041
|
|
|
1042
|
+
const SUPPORTED_DIMENSION_EXTRACTIONS = new Set([
|
|
1043
|
+
'NONE',
|
|
1044
|
+
'HOUR_OF_DAY',
|
|
1045
|
+
'DAY_OF_WEEK',
|
|
1046
|
+
'DAY_OF_MONTH',
|
|
1047
|
+
'MONTH_OF_YEAR',
|
|
1048
|
+
'QUARTER_OF_YEAR',
|
|
1049
|
+
'HOUR',
|
|
1050
|
+
'DAY',
|
|
1051
|
+
'WEEK',
|
|
1052
|
+
'MONTH',
|
|
1053
|
+
'QUARTER',
|
|
1054
|
+
'YEAR',
|
|
1055
|
+
]);
|
|
1056
|
+
const SUPPORTED_PERIOD_PRESETS = new Set([
|
|
1057
|
+
'TODAY',
|
|
1058
|
+
'YESTERDAY',
|
|
1059
|
+
'LAST_7_DAYS',
|
|
1060
|
+
'LAST_30_DAYS',
|
|
1061
|
+
'THIS_MONTH',
|
|
1062
|
+
'LAST_MONTH',
|
|
1063
|
+
'THIS_QUARTER',
|
|
1064
|
+
'THIS_YEAR',
|
|
1065
|
+
]);
|
|
668
1066
|
class ChartContractValidationService {
|
|
669
1067
|
validate(document) {
|
|
670
1068
|
const issues = [];
|
|
@@ -748,12 +1146,34 @@ class ChartContractValidationService {
|
|
|
748
1146
|
if (!metric.field?.trim()) {
|
|
749
1147
|
issues.push(this.error('metric-field-required', `metrics[${index}].field`, 'x-ui.chart metric.field is required.'));
|
|
750
1148
|
}
|
|
1149
|
+
if (metric.quickView !== undefined) {
|
|
1150
|
+
this.validateQuickViewBinding(metric.quickView, `metrics[${index}].quickView`, issues);
|
|
1151
|
+
}
|
|
751
1152
|
});
|
|
752
1153
|
document.metrics
|
|
753
1154
|
?.filter((metric) => metric.aggregation === 'distinct-count' && !metric.field?.trim())
|
|
754
1155
|
.forEach((_metric, index) => {
|
|
755
1156
|
issues.push(this.error('distinct-count-missing-field', `metrics[${index}].field`, 'x-ui.chart aggregation "distinct-count" requires a metric field.'));
|
|
756
1157
|
});
|
|
1158
|
+
document.metrics?.forEach((metric, index) => {
|
|
1159
|
+
if (metric.aggregation !== 'avg-per-cycle-occurrence') {
|
|
1160
|
+
return;
|
|
1161
|
+
}
|
|
1162
|
+
if (document.source.kind !== 'praxis.stats' || document.source.operation !== 'group-by') {
|
|
1163
|
+
issues.push(this.error('avg-per-cycle-occurrence-operation-unsupported', `metrics[${index}].aggregation`, 'x-ui.chart aggregation "avg-per-cycle-occurrence" requires source.kind="praxis.stats" and source.operation="group-by".'));
|
|
1164
|
+
}
|
|
1165
|
+
const hasTemporalExtraction = (document.dimensions ?? []).some((dimension) => Boolean(dimension.extraction && dimension.extraction !== 'NONE'));
|
|
1166
|
+
if (!hasTemporalExtraction) {
|
|
1167
|
+
issues.push(this.error('avg-per-cycle-occurrence-missing-temporal-extraction', 'dimensions', 'x-ui.chart aggregation "avg-per-cycle-occurrence" requires at least one dimension with a temporal extraction.'));
|
|
1168
|
+
}
|
|
1169
|
+
if (!document.source.options?.timezone?.trim()) {
|
|
1170
|
+
issues.push(this.error('avg-per-cycle-occurrence-missing-timezone', 'source.options.timezone', 'x-ui.chart aggregation "avg-per-cycle-occurrence" requires source.options.timezone.'));
|
|
1171
|
+
}
|
|
1172
|
+
const periodPreset = document.source.options?.periodPreset;
|
|
1173
|
+
if (!periodPreset || !SUPPORTED_PERIOD_PRESETS.has(periodPreset)) {
|
|
1174
|
+
issues.push(this.error('avg-per-cycle-occurrence-missing-period-preset', 'source.options.periodPreset', 'x-ui.chart aggregation "avg-per-cycle-occurrence" requires a supported source.options.periodPreset.'));
|
|
1175
|
+
}
|
|
1176
|
+
});
|
|
757
1177
|
}
|
|
758
1178
|
validateComparison(document, issues) {
|
|
759
1179
|
if (document.source.kind !== 'praxis.stats' || document.source.operation !== 'comparison') {
|
|
@@ -842,6 +1262,7 @@ class ChartContractValidationService {
|
|
|
842
1262
|
'pyramid',
|
|
843
1263
|
'treemap',
|
|
844
1264
|
'gauge',
|
|
1265
|
+
'heatmap',
|
|
845
1266
|
];
|
|
846
1267
|
if (!supportedKinds.includes(document.kind)) {
|
|
847
1268
|
issues.push(this.error('unsupported-kind', 'kind', `x-ui.chart kind="${document.kind}" is not supported in @praxisui/charts.`));
|
|
@@ -852,7 +1273,15 @@ class ChartContractValidationService {
|
|
|
852
1273
|
if (!dimension.field?.trim()) {
|
|
853
1274
|
issues.push(this.error('dimension-field-required', `dimensions[${index}].field`, 'x-ui.chart dimension.field is required.'));
|
|
854
1275
|
}
|
|
1276
|
+
if (dimension.extraction !== undefined && !SUPPORTED_DIMENSION_EXTRACTIONS.has(dimension.extraction)) {
|
|
1277
|
+
issues.push(this.error('dimension-extraction-unsupported', `dimensions[${index}].extraction`, `x-ui.chart dimensions[${index}].extraction="${String(dimension.extraction)}" is not supported.`));
|
|
1278
|
+
}
|
|
855
1279
|
});
|
|
1280
|
+
if (document.source.kind === 'praxis.stats'
|
|
1281
|
+
&& document.source.operation === 'group-by'
|
|
1282
|
+
&& (document.dimensions?.length ?? 0) > 2) {
|
|
1283
|
+
issues.push(this.error('group-by-max-dimensions', 'dimensions', 'x-ui.chart praxis.stats group-by supports at most 2 dimensions.'));
|
|
1284
|
+
}
|
|
856
1285
|
if (!categorySliceChart && document.kind !== 'gauge' && !document.dimensions?.length) {
|
|
857
1286
|
issues.push(this.error('missing-dimension', 'dimensions', 'x-ui.chart cartesian charts require at least one dimension.'));
|
|
858
1287
|
}
|
|
@@ -866,6 +1295,7 @@ class ChartContractValidationService {
|
|
|
866
1295
|
issues.push(this.error('combo-min-metrics', 'metrics', 'x-ui.chart combo charts require at least two metrics.'));
|
|
867
1296
|
}
|
|
868
1297
|
this.validateGauge(document, issues);
|
|
1298
|
+
this.validateHeatmap(document, issues);
|
|
869
1299
|
if (document.kind !== 'combo' && document.metrics?.some((metric) => metric.axis === 'secondary')) {
|
|
870
1300
|
issues.push(this.error('secondary-axis-non-combo', 'metrics', 'x-ui.chart axis="secondary" is supported only for combo charts in @praxisui/charts.'));
|
|
871
1301
|
}
|
|
@@ -943,6 +1373,21 @@ class ChartContractValidationService {
|
|
|
943
1373
|
issues.push(this.error('gauge-limit-required', 'source.options.limit', 'x-ui.chart gauge charts over praxis.stats require source.options.limit=1.'));
|
|
944
1374
|
}
|
|
945
1375
|
}
|
|
1376
|
+
validateHeatmap(document, issues) {
|
|
1377
|
+
if (document.kind !== 'heatmap') {
|
|
1378
|
+
return;
|
|
1379
|
+
}
|
|
1380
|
+
if ((document.dimensions?.length ?? 0) !== 2) {
|
|
1381
|
+
issues.push(this.error('heatmap-two-dimensions', 'dimensions', 'x-ui.chart heatmap charts require exactly two dimensions (x and y matrix axes).'));
|
|
1382
|
+
}
|
|
1383
|
+
if ((document.metrics?.length ?? 0) !== 1) {
|
|
1384
|
+
issues.push(this.error('heatmap-single-metric', 'metrics', 'x-ui.chart heatmap charts require exactly one metric.'));
|
|
1385
|
+
}
|
|
1386
|
+
if (document.source.kind === 'praxis.stats'
|
|
1387
|
+
&& document.source.operation !== 'group-by') {
|
|
1388
|
+
issues.push(this.error('heatmap-operation-unsupported', 'source.operation', 'x-ui.chart heatmap charts over praxis.stats require operation="group-by".'));
|
|
1389
|
+
}
|
|
1390
|
+
}
|
|
946
1391
|
isCategorySliceChart(kind) {
|
|
947
1392
|
return kind === 'pie'
|
|
948
1393
|
|| kind === 'donut'
|
|
@@ -997,6 +1442,15 @@ class ChartContractValidationService {
|
|
|
997
1442
|
this.validateEventAction('drillDown', document.events?.drillDown, issues);
|
|
998
1443
|
this.validateEventAction('selectionChange', document.events?.selectionChange, issues);
|
|
999
1444
|
this.validateEventAction('crossFilter', document.events?.crossFilter, issues);
|
|
1445
|
+
if (document.events?.quickView !== undefined) {
|
|
1446
|
+
this.validateQuickViewBinding(document.events.quickView, 'events.quickView', issues);
|
|
1447
|
+
}
|
|
1448
|
+
}
|
|
1449
|
+
validateQuickViewBinding(binding, field, issues) {
|
|
1450
|
+
const result = validateResourceQuickViewBinding(binding);
|
|
1451
|
+
if (!result.valid || !result.binding) {
|
|
1452
|
+
issues.push(this.error('quick-view-binding-invalid', field, `x-ui.chart ${field} must be a valid ResourceQuickViewBinding with recordOpen.sourceIdentityField and recordOpen.target.{resourceKey, surfaceId}.`));
|
|
1453
|
+
}
|
|
1000
1454
|
}
|
|
1001
1455
|
validateEventAction(eventKey, eventAction, issues) {
|
|
1002
1456
|
if (!eventAction?.action) {
|
|
@@ -1199,7 +1653,15 @@ class PraxisChartCanonicalContractMapperService {
|
|
|
1199
1653
|
return this.normalizer.normalize(contract);
|
|
1200
1654
|
}
|
|
1201
1655
|
buildAxes(contract) {
|
|
1202
|
-
const
|
|
1656
|
+
const dimensions = contract.dimensions ?? [];
|
|
1657
|
+
const firstDimension = dimensions[0];
|
|
1658
|
+
const secondDimension = dimensions[1];
|
|
1659
|
+
const firstDimensionField = firstDimension
|
|
1660
|
+
? this.resolveEffectiveDimensionField(firstDimension, dimensions)
|
|
1661
|
+
: undefined;
|
|
1662
|
+
const secondDimensionField = secondDimension
|
|
1663
|
+
? this.resolveEffectiveDimensionField(secondDimension, dimensions)
|
|
1664
|
+
: undefined;
|
|
1203
1665
|
const firstMetric = contract.metrics?.[0];
|
|
1204
1666
|
const secondMetric = contract.metrics?.[1];
|
|
1205
1667
|
const secondaryMetric = contract.metrics?.find((metric) => metric.axis === 'secondary');
|
|
@@ -1208,16 +1670,30 @@ class PraxisChartCanonicalContractMapperService {
|
|
|
1208
1670
|
if (this.isCategorySliceChart(contract.kind) || contract.kind === 'gauge') {
|
|
1209
1671
|
return {
|
|
1210
1672
|
x: {
|
|
1211
|
-
field:
|
|
1673
|
+
field: firstDimensionField,
|
|
1212
1674
|
label: this.toLabel(firstDimension?.label),
|
|
1213
1675
|
},
|
|
1214
1676
|
};
|
|
1215
1677
|
}
|
|
1678
|
+
if (contract.kind === 'heatmap') {
|
|
1679
|
+
return {
|
|
1680
|
+
x: {
|
|
1681
|
+
field: firstDimensionField,
|
|
1682
|
+
label: this.toLabel(firstDimension?.label),
|
|
1683
|
+
type: 'category',
|
|
1684
|
+
},
|
|
1685
|
+
y: {
|
|
1686
|
+
field: secondDimensionField,
|
|
1687
|
+
label: this.toLabel(secondDimension?.label),
|
|
1688
|
+
type: 'category',
|
|
1689
|
+
},
|
|
1690
|
+
};
|
|
1691
|
+
}
|
|
1216
1692
|
if (contract.kind === 'scatter') {
|
|
1217
1693
|
const groupedMeasures = !!secondMetric;
|
|
1218
1694
|
return {
|
|
1219
1695
|
x: {
|
|
1220
|
-
field: groupedMeasures ? firstMetric?.field :
|
|
1696
|
+
field: groupedMeasures ? firstMetric?.field : firstDimensionField,
|
|
1221
1697
|
label: this.toLabel(groupedMeasures ? firstMetric?.label : firstDimension?.label),
|
|
1222
1698
|
type: groupedMeasures ? 'value' : firstDimension?.role === 'time' ? 'time' : 'value',
|
|
1223
1699
|
},
|
|
@@ -1230,7 +1706,7 @@ class PraxisChartCanonicalContractMapperService {
|
|
|
1230
1706
|
}
|
|
1231
1707
|
return {
|
|
1232
1708
|
x: {
|
|
1233
|
-
field:
|
|
1709
|
+
field: firstDimensionField,
|
|
1234
1710
|
label: this.toLabel(firstDimension?.label),
|
|
1235
1711
|
type: firstDimension?.role === 'time' ? 'time' : 'category',
|
|
1236
1712
|
},
|
|
@@ -1251,7 +1727,15 @@ class PraxisChartCanonicalContractMapperService {
|
|
|
1251
1727
|
};
|
|
1252
1728
|
}
|
|
1253
1729
|
buildSeries(contract) {
|
|
1254
|
-
const
|
|
1730
|
+
const dimensions = contract.dimensions ?? [];
|
|
1731
|
+
const firstDimension = dimensions[0];
|
|
1732
|
+
const secondDimension = dimensions[1];
|
|
1733
|
+
const firstDimensionField = firstDimension
|
|
1734
|
+
? this.resolveEffectiveDimensionField(firstDimension, dimensions)
|
|
1735
|
+
: undefined;
|
|
1736
|
+
const secondDimensionField = secondDimension
|
|
1737
|
+
? this.resolveEffectiveDimensionField(secondDimension, dimensions)
|
|
1738
|
+
: undefined;
|
|
1255
1739
|
const labelsVisible = this.resolveOptionalToggle(contract.labels);
|
|
1256
1740
|
const comparison = contract.source.kind === 'praxis.stats' && contract.source.operation === 'comparison';
|
|
1257
1741
|
const metrics = contract.kind === 'scatter' && (contract.metrics?.length ?? 0) === 2
|
|
@@ -1266,8 +1750,9 @@ class PraxisChartCanonicalContractMapperService {
|
|
|
1266
1750
|
type: this.resolveSeriesType(contract.kind, metric.seriesKind, index),
|
|
1267
1751
|
axis: metric.axis ?? 'primary',
|
|
1268
1752
|
categoryField: comparison || this.isCategorySliceChart(contract.kind) || contract.kind === 'gauge'
|
|
1269
|
-
?
|
|
1753
|
+
? firstDimensionField
|
|
1270
1754
|
: undefined,
|
|
1755
|
+
...(secondDimensionField ? { secondaryCategoryField: secondDimensionField } : {}),
|
|
1271
1756
|
metric: {
|
|
1272
1757
|
field: period ? this.comparisonMetricField(metric.field, period) : metric.field,
|
|
1273
1758
|
aggregation: this.mapAggregation(metric.aggregation),
|
|
@@ -1282,6 +1767,7 @@ class PraxisChartCanonicalContractMapperService {
|
|
|
1282
1767
|
}
|
|
1283
1768
|
: undefined,
|
|
1284
1769
|
smooth: this.shouldSmoothSeries(contract.kind, metric.seriesKind),
|
|
1770
|
+
...(metric.quickView ? { quickView: structuredClone(metric.quickView) } : {}),
|
|
1285
1771
|
}));
|
|
1286
1772
|
});
|
|
1287
1773
|
}
|
|
@@ -1304,18 +1790,26 @@ class PraxisChartCanonicalContractMapperService {
|
|
|
1304
1790
|
const combinedFilters = {
|
|
1305
1791
|
...filtersFromContract,
|
|
1306
1792
|
};
|
|
1793
|
+
const dimensions = contract.dimensions ?? [];
|
|
1794
|
+
const dimensionConfigs = this.buildDimensionConfigs(dimensions);
|
|
1307
1795
|
return {
|
|
1308
1796
|
sourceKind: 'praxis.stats',
|
|
1309
1797
|
statsOperation: contract.source.operation,
|
|
1310
1798
|
granularity: contract.source.options?.granularity,
|
|
1311
1799
|
fillGaps: contract.source.options?.fillGaps,
|
|
1800
|
+
...(contract.source.options?.timezone ? { timezone: contract.source.options.timezone } : {}),
|
|
1801
|
+
...(contract.source.options?.periodField ? { periodField: contract.source.options.periodField } : {}),
|
|
1802
|
+
...(contract.source.options?.periodPreset ? { periodPreset: contract.source.options.periodPreset } : {}),
|
|
1312
1803
|
distributionMode: contract.source.options?.mode,
|
|
1313
1804
|
bucketSize: contract.source.options?.bucketSize,
|
|
1314
1805
|
bucketCount: contract.source.options?.bucketCount,
|
|
1315
1806
|
statsOrderBy: this.mapStatsOrderBy(contract.source.options?.orderBy),
|
|
1316
1807
|
statsPath: this.buildStatsPath(contract),
|
|
1317
1808
|
statsRequest: this.buildStatsRequest(contract, combinedFilters),
|
|
1318
|
-
dimensions:
|
|
1809
|
+
dimensions: dimensions.length
|
|
1810
|
+
? dimensions.map((dimension) => this.resolveEffectiveDimensionField(dimension, dimensions))
|
|
1811
|
+
: undefined,
|
|
1812
|
+
...(dimensionConfigs?.length ? { dimensionConfigs } : {}),
|
|
1319
1813
|
metrics: contract.source.operation === 'comparison'
|
|
1320
1814
|
? contract.metrics?.flatMap((metric) => ['current', 'previous'].map((period) => ({
|
|
1321
1815
|
field: this.comparisonMetricField(metric.field, period),
|
|
@@ -1338,6 +1832,7 @@ class PraxisChartCanonicalContractMapperService {
|
|
|
1338
1832
|
selection: Boolean(contract.events?.selectionChange),
|
|
1339
1833
|
drillDown: Boolean(contract.events?.drillDown),
|
|
1340
1834
|
crossFilter: Boolean(contract.events?.crossFilter),
|
|
1835
|
+
...(contract.events?.quickView ? { quickView: structuredClone(contract.events.quickView) } : {}),
|
|
1341
1836
|
eventActions: {
|
|
1342
1837
|
pointClick: contract.events?.pointClick,
|
|
1343
1838
|
selectionChange: contract.events?.selectionChange,
|
|
@@ -1362,7 +1857,7 @@ class PraxisChartCanonicalContractMapperService {
|
|
|
1362
1857
|
tooltip: {
|
|
1363
1858
|
...(variantTheme?.tooltip ?? {}),
|
|
1364
1859
|
enabled: this.resolveToggle(contract.tooltip, true),
|
|
1365
|
-
trigger: this.isCategorySliceChart(contract.kind) || contract.kind === 'scatter' || contract.kind === 'gauge' ? 'item' : 'axis',
|
|
1860
|
+
trigger: this.isCategorySliceChart(contract.kind) || contract.kind === 'scatter' || contract.kind === 'gauge' || contract.kind === 'heatmap' ? 'item' : 'axis',
|
|
1366
1861
|
},
|
|
1367
1862
|
surface,
|
|
1368
1863
|
backgroundColor: surface?.background ?? variantTheme?.backgroundColor,
|
|
@@ -1491,6 +1986,7 @@ class PraxisChartCanonicalContractMapperService {
|
|
|
1491
1986
|
case 'count':
|
|
1492
1987
|
case 'distinct-count':
|
|
1493
1988
|
case 'sum':
|
|
1989
|
+
case 'avg-per-cycle-occurrence':
|
|
1494
1990
|
return aggregation;
|
|
1495
1991
|
case undefined:
|
|
1496
1992
|
return undefined;
|
|
@@ -1523,15 +2019,24 @@ class PraxisChartCanonicalContractMapperService {
|
|
|
1523
2019
|
const orderBy = this.mapStatsOrderByToBackend(contract.source.options?.orderBy);
|
|
1524
2020
|
const requestMetrics = metrics.length > 1 ? metrics : undefined;
|
|
1525
2021
|
switch (contract.source.operation) {
|
|
1526
|
-
case 'group-by':
|
|
2022
|
+
case 'group-by': {
|
|
2023
|
+
const dimensionConfigs = this.buildDimensionConfigs(contract.dimensions ?? []);
|
|
2024
|
+
const includeDimensions = (dimensionConfigs?.length ?? 0) > 1
|
|
2025
|
+
|| Boolean(dimensionConfigs?.some((item) => (item.extraction && item.extraction !== 'NONE') || item.alias));
|
|
1527
2026
|
return {
|
|
1528
2027
|
filter: filters,
|
|
1529
|
-
field,
|
|
2028
|
+
field: (dimensionConfigs?.length ?? 0) > 1 ? undefined : field,
|
|
2029
|
+
...(includeDimensions && dimensionConfigs ? { dimensions: dimensionConfigs } : {}),
|
|
1530
2030
|
metric,
|
|
1531
2031
|
metrics: requestMetrics,
|
|
2032
|
+
...(contract.source.options?.timezone ? { timezone: contract.source.options.timezone } : {}),
|
|
2033
|
+
...(contract.source.options?.periodField ? { periodField: contract.source.options.periodField } : {}),
|
|
2034
|
+
...(contract.source.options?.periodPreset ? { periodPreset: contract.source.options.periodPreset } : {}),
|
|
2035
|
+
...(contract.source.options?.fillGaps !== undefined ? { fillGaps: contract.source.options.fillGaps } : {}),
|
|
1532
2036
|
limit,
|
|
1533
2037
|
orderBy,
|
|
1534
2038
|
};
|
|
2039
|
+
}
|
|
1535
2040
|
case 'timeseries':
|
|
1536
2041
|
return {
|
|
1537
2042
|
filter: filters,
|
|
@@ -1584,24 +2089,66 @@ class PraxisChartCanonicalContractMapperService {
|
|
|
1584
2089
|
if (!metric) {
|
|
1585
2090
|
throw new Error('x-ui.chart requires metrics[0] to derive the canonical praxis.stats request.');
|
|
1586
2091
|
}
|
|
1587
|
-
|
|
1588
|
-
return {
|
|
1589
|
-
operation,
|
|
1590
|
-
field: operation === 'COUNT' ? undefined : metric.field,
|
|
1591
|
-
alias: metric.field,
|
|
1592
|
-
};
|
|
2092
|
+
return this.toStatsMetricRequest(metric);
|
|
1593
2093
|
}
|
|
1594
2094
|
buildStatsMetricRequests(contract) {
|
|
1595
2095
|
if (!contract.metrics?.length) {
|
|
1596
2096
|
throw new Error('x-ui.chart requires at least one metric to derive the canonical praxis.stats request.');
|
|
1597
2097
|
}
|
|
1598
|
-
return contract.metrics.map((metric) =>
|
|
1599
|
-
|
|
2098
|
+
return contract.metrics.map((metric) => this.toStatsMetricRequest(metric));
|
|
2099
|
+
}
|
|
2100
|
+
toStatsMetricRequest(metric) {
|
|
2101
|
+
const operation = this.mapStatsMetricOperation(metric.aggregation);
|
|
2102
|
+
const sourceField = metric.sourceField?.trim();
|
|
2103
|
+
if (operation === 'COUNT') {
|
|
2104
|
+
return {
|
|
2105
|
+
operation,
|
|
2106
|
+
field: undefined,
|
|
2107
|
+
alias: metric.field,
|
|
2108
|
+
};
|
|
2109
|
+
}
|
|
2110
|
+
if (operation === 'AVG_PER_CYCLE_OCCURRENCE') {
|
|
1600
2111
|
return {
|
|
1601
2112
|
operation,
|
|
1602
|
-
field:
|
|
2113
|
+
field: sourceField || undefined,
|
|
1603
2114
|
alias: metric.field,
|
|
1604
2115
|
};
|
|
2116
|
+
}
|
|
2117
|
+
return {
|
|
2118
|
+
operation,
|
|
2119
|
+
field: sourceField || metric.field,
|
|
2120
|
+
alias: metric.field,
|
|
2121
|
+
};
|
|
2122
|
+
}
|
|
2123
|
+
resolveEffectiveDimensionField(dimension, allDimensions = [dimension]) {
|
|
2124
|
+
const explicitAlias = dimension.alias?.trim();
|
|
2125
|
+
if (explicitAlias) {
|
|
2126
|
+
return explicitAlias;
|
|
2127
|
+
}
|
|
2128
|
+
const field = dimension.field.trim();
|
|
2129
|
+
const extraction = dimension.extraction && dimension.extraction !== 'NONE' ? dimension.extraction : undefined;
|
|
2130
|
+
if (!extraction) {
|
|
2131
|
+
return field;
|
|
2132
|
+
}
|
|
2133
|
+
const sameFieldCount = allDimensions.filter((item) => item.field?.trim() === field).length;
|
|
2134
|
+
return sameFieldCount > 1 ? `${field}_${extraction.toLowerCase()}` : field;
|
|
2135
|
+
}
|
|
2136
|
+
buildDimensionConfigs(dimensions) {
|
|
2137
|
+
if (!dimensions.length) {
|
|
2138
|
+
return undefined;
|
|
2139
|
+
}
|
|
2140
|
+
return dimensions.map((dimension) => {
|
|
2141
|
+
const effectiveAlias = this.resolveEffectiveDimensionField(dimension, dimensions);
|
|
2142
|
+
const extraction = dimension.extraction && dimension.extraction !== 'NONE' ? dimension.extraction : undefined;
|
|
2143
|
+
return {
|
|
2144
|
+
field: dimension.field.trim(),
|
|
2145
|
+
...(extraction ? { extraction } : {}),
|
|
2146
|
+
...(dimension.alias?.trim()
|
|
2147
|
+
? { alias: dimension.alias.trim() }
|
|
2148
|
+
: effectiveAlias !== dimension.field.trim()
|
|
2149
|
+
? { alias: effectiveAlias }
|
|
2150
|
+
: {}),
|
|
2151
|
+
};
|
|
1605
2152
|
});
|
|
1606
2153
|
}
|
|
1607
2154
|
mapStatsOrderBy(value) {
|
|
@@ -1672,6 +2219,8 @@ class PraxisChartCanonicalContractMapperService {
|
|
|
1672
2219
|
return 'MIN';
|
|
1673
2220
|
case 'max':
|
|
1674
2221
|
return 'MAX';
|
|
2222
|
+
case 'avg-per-cycle-occurrence':
|
|
2223
|
+
return 'AVG_PER_CYCLE_OCCURRENCE';
|
|
1675
2224
|
default:
|
|
1676
2225
|
throw new Error(`x-ui.chart aggregation "${aggregation}" is not supported in @praxisui/charts.`);
|
|
1677
2226
|
}
|
|
@@ -1712,9 +2261,12 @@ class PraxisChartCanonicalContractMapperService {
|
|
|
1712
2261
|
case 'min':
|
|
1713
2262
|
case 'max':
|
|
1714
2263
|
case 'distinct-count':
|
|
2264
|
+
case 'avg-per-cycle-occurrence':
|
|
1715
2265
|
return lower;
|
|
1716
2266
|
case 'distinct_count':
|
|
1717
2267
|
return 'distinct-count';
|
|
2268
|
+
case 'avg_per_cycle_occurrence':
|
|
2269
|
+
return 'avg-per-cycle-occurrence';
|
|
1718
2270
|
default:
|
|
1719
2271
|
return undefined;
|
|
1720
2272
|
}
|
|
@@ -1816,6 +2368,20 @@ class PraxisChartCanonicalContractMapperService {
|
|
|
1816
2368
|
mode: comparisonPeriod.mode ?? 'PREVIOUS_ALIGNED',
|
|
1817
2369
|
};
|
|
1818
2370
|
}
|
|
2371
|
+
if (operation === 'group-by') {
|
|
2372
|
+
const timezone = query.timezone ?? query.statsRequest?.timezone;
|
|
2373
|
+
if (typeof timezone === 'string' && timezone.trim()) {
|
|
2374
|
+
options.timezone = timezone.trim();
|
|
2375
|
+
}
|
|
2376
|
+
const groupByPeriodField = query.periodField ?? query.statsRequest?.periodField;
|
|
2377
|
+
if (typeof groupByPeriodField === 'string' && groupByPeriodField.trim()) {
|
|
2378
|
+
options.periodField = groupByPeriodField.trim();
|
|
2379
|
+
}
|
|
2380
|
+
const periodPreset = query.periodPreset ?? query.statsRequest?.periodPreset;
|
|
2381
|
+
if (typeof periodPreset === 'string' && periodPreset.trim()) {
|
|
2382
|
+
options.periodPreset = periodPreset.trim();
|
|
2383
|
+
}
|
|
2384
|
+
}
|
|
1819
2385
|
return {
|
|
1820
2386
|
kind: 'praxis.stats',
|
|
1821
2387
|
resource,
|
|
@@ -1847,6 +2413,21 @@ class PraxisChartCanonicalContractMapperService {
|
|
|
1847
2413
|
: (config.axes?.x?.type === 'value' ? 'value' : 'category');
|
|
1848
2414
|
const label = this.toCanonicalTextValue(config.axes?.x?.label);
|
|
1849
2415
|
const format = config.axes?.x?.labels?.format;
|
|
2416
|
+
const dimensionConfigs = (remoteQuery?.dimensionConfigs
|
|
2417
|
+
?? remoteQuery?.statsRequest?.dimensions);
|
|
2418
|
+
if (dimensionConfigs?.length) {
|
|
2419
|
+
return dimensionConfigs.map((dimConfig, index) => ({
|
|
2420
|
+
field: dimConfig.field,
|
|
2421
|
+
...(index === 0 && label !== undefined ? { label } : {}),
|
|
2422
|
+
...(index === 1 && config.axes?.y?.type === 'category' && config.axes?.y?.label !== undefined
|
|
2423
|
+
? { label: this.toCanonicalTextValue(config.axes.y.label) }
|
|
2424
|
+
: {}),
|
|
2425
|
+
role: index === 0 ? role : 'category',
|
|
2426
|
+
...(index === 0 && format ? { format } : {}),
|
|
2427
|
+
...(dimConfig.extraction ? { extraction: dimConfig.extraction } : {}),
|
|
2428
|
+
...(dimConfig.alias ? { alias: dimConfig.alias } : {}),
|
|
2429
|
+
}));
|
|
2430
|
+
}
|
|
1850
2431
|
const primaryDimension = {
|
|
1851
2432
|
field: primaryField,
|
|
1852
2433
|
...(label !== undefined ? { label } : {}),
|
|
@@ -1857,10 +2438,22 @@ class PraxisChartCanonicalContractMapperService {
|
|
|
1857
2438
|
.slice(1)
|
|
1858
2439
|
.map((f) => f.trim())
|
|
1859
2440
|
.filter((f) => Boolean(f) && f !== primaryField);
|
|
2441
|
+
const seriesSecondaryField = config.series?.[0]?.secondaryCategoryField?.trim()
|
|
2442
|
+
|| (config.axes?.y?.type === 'category' ? config.axes.y.field?.trim() : undefined);
|
|
2443
|
+
if (!extraFields.length && seriesSecondaryField && seriesSecondaryField !== primaryField) {
|
|
2444
|
+
extraFields.push(seriesSecondaryField);
|
|
2445
|
+
}
|
|
1860
2446
|
if (extraFields.length) {
|
|
2447
|
+
const secondaryLabel = config.axes?.y?.type === 'category'
|
|
2448
|
+
? this.toCanonicalTextValue(config.axes.y.label)
|
|
2449
|
+
: undefined;
|
|
1861
2450
|
return [
|
|
1862
2451
|
primaryDimension,
|
|
1863
|
-
...extraFields.map((f) => ({
|
|
2452
|
+
...extraFields.map((f, idx) => ({
|
|
2453
|
+
field: f,
|
|
2454
|
+
role: 'category',
|
|
2455
|
+
...(idx === 0 && secondaryLabel !== undefined ? { label: secondaryLabel } : {}),
|
|
2456
|
+
})),
|
|
1864
2457
|
];
|
|
1865
2458
|
}
|
|
1866
2459
|
return [primaryDimension];
|
|
@@ -1886,6 +2479,7 @@ class PraxisChartCanonicalContractMapperService {
|
|
|
1886
2479
|
...(axis ? { axis } : {}),
|
|
1887
2480
|
...(color ? { color } : {}),
|
|
1888
2481
|
...(format ? { format } : {}),
|
|
2482
|
+
...(s.quickView ? { quickView: structuredClone(s.quickView) } : {}),
|
|
1889
2483
|
};
|
|
1890
2484
|
});
|
|
1891
2485
|
}
|
|
@@ -1925,12 +2519,13 @@ class PraxisChartCanonicalContractMapperService {
|
|
|
1925
2519
|
return undefined;
|
|
1926
2520
|
}
|
|
1927
2521
|
extractCanonicalEvents(config) {
|
|
1928
|
-
if (config.interactions?.eventActions) {
|
|
2522
|
+
if (config.interactions?.eventActions || config.interactions?.quickView) {
|
|
1929
2523
|
return {
|
|
1930
|
-
pointClick: config.interactions
|
|
1931
|
-
selectionChange: config.interactions
|
|
1932
|
-
drillDown: config.interactions
|
|
1933
|
-
crossFilter: config.interactions
|
|
2524
|
+
pointClick: config.interactions?.eventActions?.pointClick,
|
|
2525
|
+
selectionChange: config.interactions?.eventActions?.selectionChange,
|
|
2526
|
+
drillDown: config.interactions?.eventActions?.drillDown,
|
|
2527
|
+
crossFilter: config.interactions?.eventActions?.crossFilter,
|
|
2528
|
+
...(config.interactions?.quickView ? { quickView: structuredClone(config.interactions.quickView) } : {}),
|
|
1934
2529
|
};
|
|
1935
2530
|
}
|
|
1936
2531
|
if (config.interactions?.crossFilter) {
|
|
@@ -2043,6 +2638,12 @@ class PraxisChartStatsApiService {
|
|
|
2043
2638
|
};
|
|
2044
2639
|
statsRequest = this.synthesizeStatsRequest(effectiveQuery, statsType);
|
|
2045
2640
|
}
|
|
2641
|
+
if (statsRequest && statsRequest.filter == null) {
|
|
2642
|
+
statsRequest = {
|
|
2643
|
+
...statsRequest,
|
|
2644
|
+
filter: query?.filters ?? {},
|
|
2645
|
+
};
|
|
2646
|
+
}
|
|
2046
2647
|
if (!statsPath || !statsRequest) {
|
|
2047
2648
|
return throwError(() => new Error('PraxisChartStatsApiService requires query.statsPath (or resourceKey) and query.statsRequest (or field) for praxis.stats execution.'));
|
|
2048
2649
|
}
|
|
@@ -2094,11 +2695,17 @@ class PraxisChartStatsApiService {
|
|
|
2094
2695
|
}
|
|
2095
2696
|
case 'group-by':
|
|
2096
2697
|
default: {
|
|
2698
|
+
const dimensionConfigs = query.dimensionConfigs;
|
|
2097
2699
|
return {
|
|
2098
2700
|
filter,
|
|
2099
|
-
field,
|
|
2701
|
+
field: (dimensionConfigs?.length ?? 0) > 1 ? undefined : field,
|
|
2702
|
+
...(dimensionConfigs?.length ? { dimensions: dimensionConfigs } : {}),
|
|
2100
2703
|
metric,
|
|
2101
2704
|
...(query.metrics?.length ? { metrics: this.normalizeMetricRequests(query.metrics) } : {}),
|
|
2705
|
+
...(query.timezone ? { timezone: query.timezone } : {}),
|
|
2706
|
+
...(query.periodField ? { periodField: query.periodField } : {}),
|
|
2707
|
+
...(query.periodPreset ? { periodPreset: query.periodPreset } : {}),
|
|
2708
|
+
...(query.fillGaps !== undefined ? { fillGaps: query.fillGaps } : {}),
|
|
2102
2709
|
...(query.limit !== undefined ? { limit: query.limit } : {}),
|
|
2103
2710
|
...(query.statsOrderBy ? { orderBy: this.normalizeOrderBy(query.statsOrderBy) } : {}),
|
|
2104
2711
|
};
|
|
@@ -2110,7 +2717,14 @@ class PraxisChartStatsApiService {
|
|
|
2110
2717
|
return { operation: 'COUNT' };
|
|
2111
2718
|
}
|
|
2112
2719
|
if (typeof metric === 'string') {
|
|
2113
|
-
|
|
2720
|
+
const normalized = metric.toLowerCase();
|
|
2721
|
+
return {
|
|
2722
|
+
operation: (normalized === 'distinct-count'
|
|
2723
|
+
? 'DISTINCT_COUNT'
|
|
2724
|
+
: normalized === 'avg-per-cycle-occurrence'
|
|
2725
|
+
? 'AVG_PER_CYCLE_OCCURRENCE'
|
|
2726
|
+
: metric.toUpperCase()),
|
|
2727
|
+
};
|
|
2114
2728
|
}
|
|
2115
2729
|
if ('operation' in metric && metric.operation) {
|
|
2116
2730
|
const op = metric.operation.toUpperCase();
|
|
@@ -2124,14 +2738,22 @@ class PraxisChartStatsApiService {
|
|
|
2124
2738
|
return result;
|
|
2125
2739
|
}
|
|
2126
2740
|
if ('aggregation' in metric && metric.aggregation) {
|
|
2127
|
-
const op = (metric.aggregation === 'distinct-count'
|
|
2741
|
+
const op = (metric.aggregation === 'distinct-count'
|
|
2742
|
+
? 'DISTINCT_COUNT'
|
|
2743
|
+
: metric.aggregation === 'avg-per-cycle-occurrence'
|
|
2744
|
+
? 'AVG_PER_CYCLE_OCCURRENCE'
|
|
2745
|
+
: metric.aggregation.toUpperCase());
|
|
2128
2746
|
const result = {
|
|
2129
2747
|
operation: op,
|
|
2130
2748
|
};
|
|
2131
|
-
if (metric.field && op !== 'COUNT')
|
|
2749
|
+
if (metric.field && op !== 'COUNT' && op !== 'AVG_PER_CYCLE_OCCURRENCE')
|
|
2132
2750
|
result.field = metric.field;
|
|
2133
|
-
if (metric.alias)
|
|
2751
|
+
if (metric.alias) {
|
|
2134
2752
|
result.alias = metric.alias;
|
|
2753
|
+
}
|
|
2754
|
+
else if (metric.field && (op === 'COUNT' || op === 'AVG_PER_CYCLE_OCCURRENCE')) {
|
|
2755
|
+
result.alias = metric.field;
|
|
2756
|
+
}
|
|
2135
2757
|
return result;
|
|
2136
2758
|
}
|
|
2137
2759
|
const result = { operation: 'COUNT' };
|
|
@@ -2222,13 +2844,62 @@ class PraxisChartStatsApiService {
|
|
|
2222
2844
|
});
|
|
2223
2845
|
}
|
|
2224
2846
|
if ('buckets' in response) {
|
|
2847
|
+
const primaryCoordAlias = this.resolveDimensionCoordinateAlias(0, request, response, categoryField);
|
|
2848
|
+
const secondaryCategoryField = this.resolveSecondaryCategoryField(config, request, response);
|
|
2849
|
+
const secondaryCoordAlias = this.resolveDimensionCoordinateAlias(1, request, response, secondaryCategoryField);
|
|
2225
2850
|
return response.buckets.map((bucket) => {
|
|
2226
|
-
const
|
|
2851
|
+
const isMultiDimBucket = Boolean(secondaryCategoryField
|
|
2852
|
+
&& ((bucket.keys && bucket.keys.length > 1)
|
|
2853
|
+
|| (bucket.labels && bucket.labels.length > 1)
|
|
2854
|
+
|| (bucket.coordinates && Object.keys(bucket.coordinates).length > 1)));
|
|
2855
|
+
const primaryKey = isMultiDimBucket
|
|
2856
|
+
? (bucket.keys && bucket.keys.length > 0
|
|
2857
|
+
? bucket.keys[0]
|
|
2858
|
+
: (primaryCoordAlias && bucket.coordinates && primaryCoordAlias in bucket.coordinates
|
|
2859
|
+
? bucket.coordinates[primaryCoordAlias]
|
|
2860
|
+
: (bucket.key ?? null)))
|
|
2861
|
+
: (bucket.key !== undefined
|
|
2862
|
+
? bucket.key
|
|
2863
|
+
: (bucket.keys && bucket.keys.length > 0 ? bucket.keys[0] : null));
|
|
2864
|
+
const primaryLabelRaw = isMultiDimBucket
|
|
2865
|
+
? (bucket.labels && bucket.labels.length > 0
|
|
2866
|
+
? bucket.labels[0]
|
|
2867
|
+
: (primaryKey !== null && primaryKey !== undefined ? String(primaryKey) : (bucket.label ?? null)))
|
|
2868
|
+
: (bucket.label !== undefined
|
|
2869
|
+
? bucket.label
|
|
2870
|
+
: (bucket.labels && bucket.labels.length > 0 ? bucket.labels[0] : null));
|
|
2871
|
+
const category = this.resolveBucketCategory({ key: primaryKey, label: primaryLabelRaw, from: bucket.from, to: bucket.to }, categoryField, config);
|
|
2872
|
+
if (isMultiDimBucket && secondaryCategoryField) {
|
|
2873
|
+
const secondaryKey = bucket.keys && bucket.keys.length > 1
|
|
2874
|
+
? bucket.keys[1]
|
|
2875
|
+
: (secondaryCoordAlias && bucket.coordinates ? bucket.coordinates[secondaryCoordAlias] : null);
|
|
2876
|
+
const secondaryLabelRaw = bucket.labels && bucket.labels.length > 1
|
|
2877
|
+
? bucket.labels[1]
|
|
2878
|
+
: (secondaryKey !== null && secondaryKey !== undefined ? String(secondaryKey) : null);
|
|
2879
|
+
const secondaryCategory = this.resolveBucketCategory({ key: secondaryKey, label: secondaryLabelRaw }, secondaryCategoryField, config);
|
|
2880
|
+
return {
|
|
2881
|
+
[categoryField]: category,
|
|
2882
|
+
[secondaryCategoryField]: secondaryCategory,
|
|
2883
|
+
...this.projectMetricValues(metricBindings, bucket.values, bucket.value, bucket.count),
|
|
2884
|
+
key: primaryKey ?? null,
|
|
2885
|
+
label: this.isNullishBucketToken(primaryLabelRaw) ? category : String(primaryLabelRaw),
|
|
2886
|
+
secondaryKey: secondaryKey ?? null,
|
|
2887
|
+
secondaryLabel: this.isNullishBucketToken(secondaryLabelRaw) ? secondaryCategory : String(secondaryLabelRaw),
|
|
2888
|
+
compositeKey: bucket.key ?? null,
|
|
2889
|
+
coordinates: bucket.coordinates ?? null,
|
|
2890
|
+
value: bucket.value ?? null,
|
|
2891
|
+
count: bucket.count ?? null,
|
|
2892
|
+
from: bucket.from ?? null,
|
|
2893
|
+
to: bucket.to ?? null,
|
|
2894
|
+
mode: 'mode' in response ? response.mode ?? null : null,
|
|
2895
|
+
requestMode: 'mode' in request ? request.mode : null,
|
|
2896
|
+
};
|
|
2897
|
+
}
|
|
2227
2898
|
return {
|
|
2228
2899
|
[categoryField]: category,
|
|
2229
2900
|
...this.projectMetricValues(metricBindings, bucket.values, bucket.value, bucket.count),
|
|
2230
|
-
key:
|
|
2231
|
-
label: this.isNullishBucketToken(
|
|
2901
|
+
key: primaryKey ?? null,
|
|
2902
|
+
label: this.isNullishBucketToken(primaryLabelRaw) ? category : String(primaryLabelRaw),
|
|
2232
2903
|
value: bucket.value ?? null,
|
|
2233
2904
|
count: bucket.count ?? null,
|
|
2234
2905
|
from: bucket.from ?? null,
|
|
@@ -2326,11 +2997,60 @@ class PraxisChartStatsApiService {
|
|
|
2326
2997
|
}
|
|
2327
2998
|
resolveCategoryField(config, request) {
|
|
2328
2999
|
const firstSeries = config.series[0];
|
|
3000
|
+
const firstRequestDim = 'dimensions' in request && request.dimensions?.[0]
|
|
3001
|
+
? this.resolveEffectiveStatsDimensionField(request.dimensions[0], request.dimensions)
|
|
3002
|
+
: undefined;
|
|
2329
3003
|
return (firstSeries?.categoryField
|
|
2330
3004
|
|| config.axes?.x?.field
|
|
3005
|
+
|| firstRequestDim
|
|
2331
3006
|
|| request.field
|
|
2332
3007
|
|| 'category');
|
|
2333
3008
|
}
|
|
3009
|
+
resolveSecondaryCategoryField(config, request, response) {
|
|
3010
|
+
const seriesSecondary = config.series[0]?.secondaryCategoryField?.trim();
|
|
3011
|
+
if (seriesSecondary) {
|
|
3012
|
+
return seriesSecondary;
|
|
3013
|
+
}
|
|
3014
|
+
const querySecondDim = config.dataSource?.kind === 'remote'
|
|
3015
|
+
? config.dataSource.query?.dimensions?.[1]?.trim()
|
|
3016
|
+
: undefined;
|
|
3017
|
+
if (querySecondDim) {
|
|
3018
|
+
return querySecondDim;
|
|
3019
|
+
}
|
|
3020
|
+
if ('dimensions' in request && request.dimensions?.[1]) {
|
|
3021
|
+
return this.resolveEffectiveStatsDimensionField(request.dimensions[1], request.dimensions);
|
|
3022
|
+
}
|
|
3023
|
+
if ('dimensions' in response && response.dimensions?.[1]) {
|
|
3024
|
+
return this.resolveEffectiveStatsDimensionField(response.dimensions[1], response.dimensions);
|
|
3025
|
+
}
|
|
3026
|
+
return undefined;
|
|
3027
|
+
}
|
|
3028
|
+
resolveEffectiveStatsDimensionField(dim, allDims) {
|
|
3029
|
+
if (dim.alias?.trim()) {
|
|
3030
|
+
return dim.alias.trim();
|
|
3031
|
+
}
|
|
3032
|
+
const field = dim.field.trim();
|
|
3033
|
+
const extraction = dim.extraction?.trim();
|
|
3034
|
+
const duplicateCount = allDims.filter((candidate) => candidate.field.trim() === field).length;
|
|
3035
|
+
if (duplicateCount > 1 && extraction && extraction !== 'NONE') {
|
|
3036
|
+
return `${field}_${extraction.toLowerCase()}`;
|
|
3037
|
+
}
|
|
3038
|
+
return field;
|
|
3039
|
+
}
|
|
3040
|
+
resolveDimensionCoordinateAlias(index, request, response, fallback) {
|
|
3041
|
+
const dim = ('dimensions' in response && response.dimensions?.[index])
|
|
3042
|
+
|| ('dimensions' in request && request.dimensions?.[index]);
|
|
3043
|
+
if (!dim) {
|
|
3044
|
+
return fallback;
|
|
3045
|
+
}
|
|
3046
|
+
if (dim.alias?.trim()) {
|
|
3047
|
+
return dim.alias.trim();
|
|
3048
|
+
}
|
|
3049
|
+
if (dim.extraction && dim.extraction !== 'NONE') {
|
|
3050
|
+
return `${dim.field.trim()}_${dim.extraction.toLowerCase()}`;
|
|
3051
|
+
}
|
|
3052
|
+
return dim.field.trim();
|
|
3053
|
+
}
|
|
2334
3054
|
resolveMetricBindings(config, request, response) {
|
|
2335
3055
|
const queryMetrics = config.dataSource?.kind === 'remote'
|
|
2336
3056
|
? (config.dataSource.query?.metrics ?? [])
|
|
@@ -2365,7 +3085,19 @@ class PraxisChartStatsApiService {
|
|
|
2365
3085
|
}
|
|
2366
3086
|
projectMetricValues(bindings, values, primaryValue, count) {
|
|
2367
3087
|
return bindings.reduce((acc, binding, index) => {
|
|
2368
|
-
const
|
|
3088
|
+
const hasExplicitAliasValue = Boolean(values && Object.prototype.hasOwnProperty.call(values, binding.alias));
|
|
3089
|
+
const rawValue = hasExplicitAliasValue ? values[binding.alias] : undefined;
|
|
3090
|
+
if (hasExplicitAliasValue && rawValue === null) {
|
|
3091
|
+
acc[binding.field] = null;
|
|
3092
|
+
return acc;
|
|
3093
|
+
}
|
|
3094
|
+
if (!hasExplicitAliasValue
|
|
3095
|
+
&& index === 0
|
|
3096
|
+
&& primaryValue === null
|
|
3097
|
+
&& (count === null || count === undefined || count === 0)) {
|
|
3098
|
+
acc[binding.field] = null;
|
|
3099
|
+
return acc;
|
|
3100
|
+
}
|
|
2369
3101
|
acc[binding.field] = index === 0
|
|
2370
3102
|
? this.resolveMetricValue(rawValue ?? primaryValue, count)
|
|
2371
3103
|
: this.resolveMetricValue(rawValue, null);
|
|
@@ -2467,7 +3199,9 @@ class PraxisChartComponent {
|
|
|
2467
3199
|
ngZone = inject(NgZone);
|
|
2468
3200
|
componentMetadataRegistry = inject(ComponentMetadataRegistry);
|
|
2469
3201
|
settingsPanel = inject(SETTINGS_PANEL_BRIDGE, { optional: true });
|
|
3202
|
+
quickView = inject(ResourceQuickViewService, { optional: true });
|
|
2470
3203
|
destroyRef = inject(DestroyRef);
|
|
3204
|
+
activeQuickViewAnchor = null;
|
|
2471
3205
|
resizeObserver = signal(null, ...(ngDevMode ? [{ debugName: "resizeObserver" }] : /* istanbul ignore next */ []));
|
|
2472
3206
|
resizeObserverHost = null;
|
|
2473
3207
|
shellObserver = signal(null, ...(ngDevMode ? [{ debugName: "shellObserver" }] : /* istanbul ignore next */ []));
|
|
@@ -2787,8 +3521,14 @@ class PraxisChartComponent {
|
|
|
2787
3521
|
|| this.t('praxis.charts.runtime.accessibility.dataTitle', 'Chart data'), ...(ngDevMode ? [{ debugName: "accessibleDataTitle" }] : /* istanbul ignore next */ []));
|
|
2788
3522
|
accessibleDataDescription = computed(() => this.i18n.resolve(this.renderConfig().subtitle)
|
|
2789
3523
|
|| this.t('praxis.charts.runtime.accessibility.dataDescription', 'Structured alternative for the values represented in the chart.'), ...(ngDevMode ? [{ debugName: "accessibleDataDescription" }] : /* istanbul ignore next */ []));
|
|
2790
|
-
accessibleCategoryHeading = computed(() => this.
|
|
2791
|
-
|
|
3524
|
+
accessibleCategoryHeading = computed(() => (this.renderConfig().type === 'heatmap'
|
|
3525
|
+
? this.i18n.resolve(this.renderConfig().axes?.x?.label).trim()
|
|
3526
|
+
: '')
|
|
3527
|
+
|| this.t('praxis.charts.runtime.accessibility.category', 'Category'), ...(ngDevMode ? [{ debugName: "accessibleCategoryHeading" }] : /* istanbul ignore next */ []));
|
|
3528
|
+
accessibleSeriesHeading = computed(() => (this.renderConfig().type === 'heatmap'
|
|
3529
|
+
? this.i18n.resolve(this.renderConfig().axes?.y?.label).trim()
|
|
3530
|
+
: '')
|
|
3531
|
+
|| this.t('praxis.charts.runtime.accessibility.series', 'Series'), ...(ngDevMode ? [{ debugName: "accessibleSeriesHeading" }] : /* istanbul ignore next */ []));
|
|
2792
3532
|
accessibleValueHeading = computed(() => this.t('praxis.charts.runtime.accessibility.value', 'Value'), ...(ngDevMode ? [{ debugName: "accessibleValueHeading" }] : /* istanbul ignore next */ []));
|
|
2793
3533
|
widgetShellActions = computed(() => {
|
|
2794
3534
|
if (this.loadState() !== 'ready' || !this.accessiblePoints().length) {
|
|
@@ -2878,12 +3618,16 @@ class PraxisChartComponent {
|
|
|
2878
3618
|
}
|
|
2879
3619
|
this.previousSelectionScopeSignature = signature;
|
|
2880
3620
|
this.activeSelectionSignature.set(null);
|
|
3621
|
+
this.dismissActiveQuickView('anchor-invalid');
|
|
2881
3622
|
});
|
|
2882
3623
|
effect(() => {
|
|
2883
3624
|
const nextState = this.loadState();
|
|
2884
3625
|
if (nextState !== 'ready' || !this.accessiblePoints().length) {
|
|
2885
3626
|
this.showAccessibleData.set(false);
|
|
2886
3627
|
}
|
|
3628
|
+
if (nextState !== 'ready') {
|
|
3629
|
+
this.dismissActiveQuickView('anchor-invalid');
|
|
3630
|
+
}
|
|
2887
3631
|
if (this.currentLoadState() !== nextState) {
|
|
2888
3632
|
this.currentLoadState.set(nextState);
|
|
2889
3633
|
this.loadStateChange.emit(nextState);
|
|
@@ -2973,9 +3717,12 @@ class PraxisChartComponent {
|
|
|
2973
3717
|
config: this.renderConfig(),
|
|
2974
3718
|
data: this.resolvedData(),
|
|
2975
3719
|
onPointClick: (event) => this.ngZone.run(() => this.handlePointClick(event)),
|
|
3720
|
+
onPointHoverEnter: (event) => this.ngZone.run(() => this.handlePointHoverEnter(host, event)),
|
|
3721
|
+
onPointHoverLeave: () => this.ngZone.run(() => this.handlePointHoverLeave()),
|
|
2976
3722
|
});
|
|
2977
3723
|
if (this.pendingResizeOnReady) {
|
|
2978
3724
|
this.pendingResizeOnReady = false;
|
|
3725
|
+
this.dismissActiveQuickView('anchor-invalid');
|
|
2979
3726
|
this.engine.resize();
|
|
2980
3727
|
}
|
|
2981
3728
|
if (!untracked(() => this.hasRenderedOnce())) {
|
|
@@ -2983,6 +3730,7 @@ class PraxisChartComponent {
|
|
|
2983
3730
|
}
|
|
2984
3731
|
});
|
|
2985
3732
|
this.destroyRef.onDestroy(() => {
|
|
3733
|
+
this.dismissActiveQuickView('anchor-invalid');
|
|
2986
3734
|
this.clearEditorSessionSubscriptions();
|
|
2987
3735
|
this.cancelRemoteDataRequest();
|
|
2988
3736
|
this.destroyResizeObserver();
|
|
@@ -3296,8 +4044,67 @@ class PraxisChartComponent {
|
|
|
3296
4044
|
this.renderAttempt.update((value) => value + 1);
|
|
3297
4045
|
}
|
|
3298
4046
|
toggleAccessibleData() {
|
|
4047
|
+
this.dismissActiveQuickView('anchor-invalid');
|
|
3299
4048
|
this.showAccessibleData.update((visible) => !visible);
|
|
3300
4049
|
}
|
|
4050
|
+
accessiblePointQuickViewBinding(point) {
|
|
4051
|
+
return resolveChartPointQuickView(this.renderConfig(), point)?.binding ?? null;
|
|
4052
|
+
}
|
|
4053
|
+
accessiblePointQuickViewRecord(point) {
|
|
4054
|
+
return resolveChartPointQuickView(this.renderConfig(), point)?.record ?? null;
|
|
4055
|
+
}
|
|
4056
|
+
handleShellKeydown(event) {
|
|
4057
|
+
if (event.key !== 'Escape' || !this.activeQuickViewAnchor || !this.quickView) {
|
|
4058
|
+
return;
|
|
4059
|
+
}
|
|
4060
|
+
if (this.quickView.state !== 'idle'
|
|
4061
|
+
&& this.quickView.state !== 'dismissed') {
|
|
4062
|
+
event.preventDefault();
|
|
4063
|
+
event.stopPropagation();
|
|
4064
|
+
this.dismissActiveQuickView('escape');
|
|
4065
|
+
}
|
|
4066
|
+
}
|
|
4067
|
+
handlePointHoverEnter(host, event) {
|
|
4068
|
+
if (!this.quickView || this.showAccessibleData() || this.loadState() !== 'ready') {
|
|
4069
|
+
return;
|
|
4070
|
+
}
|
|
4071
|
+
const resolved = resolveChartPointQuickView(this.renderConfig(), event);
|
|
4072
|
+
if (!resolved) {
|
|
4073
|
+
this.handlePointHoverLeave();
|
|
4074
|
+
return;
|
|
4075
|
+
}
|
|
4076
|
+
const anchor = {
|
|
4077
|
+
kind: 'virtual-rect',
|
|
4078
|
+
anchorKey: event.anchorKey,
|
|
4079
|
+
hostElement: host,
|
|
4080
|
+
focusOrigin: host,
|
|
4081
|
+
getViewportRect: () => event.getViewportRect(),
|
|
4082
|
+
isValid: () => !this.destroyRef.destroyed
|
|
4083
|
+
&& this.loadState() === 'ready'
|
|
4084
|
+
&& !this.showAccessibleData()
|
|
4085
|
+
&& host.isConnected,
|
|
4086
|
+
};
|
|
4087
|
+
this.activeQuickViewAnchor = anchor;
|
|
4088
|
+
this.quickView.openFromHover({
|
|
4089
|
+
binding: resolved.binding,
|
|
4090
|
+
sourceRecord: resolved.record,
|
|
4091
|
+
anchor,
|
|
4092
|
+
trigger: 'hover',
|
|
4093
|
+
});
|
|
4094
|
+
}
|
|
4095
|
+
handlePointHoverLeave() {
|
|
4096
|
+
if (this.activeQuickViewAnchor && this.quickView) {
|
|
4097
|
+
this.quickView.notifyTriggerLeave(this.activeQuickViewAnchor);
|
|
4098
|
+
}
|
|
4099
|
+
}
|
|
4100
|
+
dismissActiveQuickView(reason) {
|
|
4101
|
+
if (!this.activeQuickViewAnchor || !this.quickView) {
|
|
4102
|
+
this.activeQuickViewAnchor = null;
|
|
4103
|
+
return;
|
|
4104
|
+
}
|
|
4105
|
+
this.quickView.dismissIfAnchoredTo(this.activeQuickViewAnchor, reason);
|
|
4106
|
+
this.activeQuickViewAnchor = null;
|
|
4107
|
+
}
|
|
3301
4108
|
setWidgetShellActionHostActive(active) {
|
|
3302
4109
|
this.widgetShellActionHostActive.set(active);
|
|
3303
4110
|
}
|
|
@@ -3411,6 +4218,29 @@ class PraxisChartComponent {
|
|
|
3411
4218
|
buildEventFilters(config, event, mapping) {
|
|
3412
4219
|
const sourceValues = this.extractPointSourceValues(config, event);
|
|
3413
4220
|
if (!mapping || !Object.keys(mapping).length) {
|
|
4221
|
+
const categoryField = config.axes?.x?.field
|
|
4222
|
+
?? config.series.find((series) => series.categoryField)?.categoryField;
|
|
4223
|
+
const secondaryCategoryField = config.series.find((series) => series.secondaryCategoryField)?.secondaryCategoryField
|
|
4224
|
+
?? (config.axes?.y?.type === 'category' ? config.axes.y.field : undefined);
|
|
4225
|
+
const dimensionFields = new Set([categoryField, secondaryCategoryField].filter((field) => Boolean(field)));
|
|
4226
|
+
const isCanonicalStats = config.dataSource?.kind === 'remote'
|
|
4227
|
+
&& config.dataSource.query?.sourceKind === 'praxis.stats';
|
|
4228
|
+
const syntheticStatsKeys = new Set([
|
|
4229
|
+
'key',
|
|
4230
|
+
'label',
|
|
4231
|
+
'secondaryKey',
|
|
4232
|
+
'secondaryLabel',
|
|
4233
|
+
'compositeKey',
|
|
4234
|
+
'coordinates',
|
|
4235
|
+
'count',
|
|
4236
|
+
'from',
|
|
4237
|
+
'to',
|
|
4238
|
+
'mode',
|
|
4239
|
+
'requestMode',
|
|
4240
|
+
'start',
|
|
4241
|
+
'end',
|
|
4242
|
+
'granularity',
|
|
4243
|
+
]);
|
|
3414
4244
|
const metricFields = new Set();
|
|
3415
4245
|
for (const series of config.series) {
|
|
3416
4246
|
if (series.metric?.field) {
|
|
@@ -3425,8 +4255,31 @@ class PraxisChartComponent {
|
|
|
3425
4255
|
}
|
|
3426
4256
|
const defaultFilters = {};
|
|
3427
4257
|
for (const [key, value] of Object.entries(sourceValues)) {
|
|
3428
|
-
if (
|
|
3429
|
-
|
|
4258
|
+
if (metricFields.has(key)) {
|
|
4259
|
+
continue;
|
|
4260
|
+
}
|
|
4261
|
+
if (key === 'value' && !dimensionFields.has('value')) {
|
|
4262
|
+
continue;
|
|
4263
|
+
}
|
|
4264
|
+
if (key === 'secondaryCategory' && !dimensionFields.has('secondaryCategory')) {
|
|
4265
|
+
continue;
|
|
4266
|
+
}
|
|
4267
|
+
if (key === 'category' && categoryField && !dimensionFields.has('category')) {
|
|
4268
|
+
continue;
|
|
4269
|
+
}
|
|
4270
|
+
if (isCanonicalStats && syntheticStatsKeys.has(key) && !dimensionFields.has(key)) {
|
|
4271
|
+
continue;
|
|
4272
|
+
}
|
|
4273
|
+
defaultFilters[key] = value;
|
|
4274
|
+
}
|
|
4275
|
+
if (isCanonicalStats) {
|
|
4276
|
+
if (categoryField && sourceValues['key'] !== undefined && sourceValues['key'] !== null) {
|
|
4277
|
+
defaultFilters[categoryField] = sourceValues['key'];
|
|
4278
|
+
}
|
|
4279
|
+
if (secondaryCategoryField
|
|
4280
|
+
&& sourceValues['secondaryKey'] !== undefined
|
|
4281
|
+
&& sourceValues['secondaryKey'] !== null) {
|
|
4282
|
+
defaultFilters[secondaryCategoryField] = sourceValues['secondaryKey'];
|
|
3430
4283
|
}
|
|
3431
4284
|
}
|
|
3432
4285
|
return defaultFilters;
|
|
@@ -3452,6 +4305,13 @@ class PraxisChartComponent {
|
|
|
3452
4305
|
if (categoryField && values[categoryField] === undefined && event.category !== undefined) {
|
|
3453
4306
|
values[categoryField] = event.category;
|
|
3454
4307
|
}
|
|
4308
|
+
const secondaryCategoryField = config.series.find((series) => series.secondaryCategoryField)?.secondaryCategoryField
|
|
4309
|
+
?? (config.axes?.y?.type === 'category' ? config.axes.y.field : undefined);
|
|
4310
|
+
if (secondaryCategoryField
|
|
4311
|
+
&& values[secondaryCategoryField] === undefined
|
|
4312
|
+
&& values['secondaryCategory'] !== undefined) {
|
|
4313
|
+
values[secondaryCategoryField] = values['secondaryCategory'];
|
|
4314
|
+
}
|
|
3455
4315
|
const series = config.series.find((candidate) => candidate.id === event.seriesId)
|
|
3456
4316
|
?? config.series.find((candidate) => candidate.name === event.seriesName)
|
|
3457
4317
|
?? config.series[0];
|
|
@@ -3698,6 +4558,7 @@ class PraxisChartComponent {
|
|
|
3698
4558
|
}
|
|
3699
4559
|
if (typeof requestAnimationFrame !== 'function') {
|
|
3700
4560
|
this.pendingResizeOnReady = false;
|
|
4561
|
+
this.dismissActiveQuickView('anchor-invalid');
|
|
3701
4562
|
this.engine.resize();
|
|
3702
4563
|
return;
|
|
3703
4564
|
}
|
|
@@ -3713,6 +4574,7 @@ class PraxisChartComponent {
|
|
|
3713
4574
|
return;
|
|
3714
4575
|
}
|
|
3715
4576
|
this.pendingResizeOnReady = false;
|
|
4577
|
+
this.dismissActiveQuickView('anchor-invalid');
|
|
3716
4578
|
this.engine.resize();
|
|
3717
4579
|
});
|
|
3718
4580
|
}
|
|
@@ -3759,6 +4621,7 @@ class PraxisChartComponent {
|
|
|
3759
4621
|
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 }, resourceKey: { classPropertyName: "resourceKey", publicName: "resourceKey", 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", chartDocumentPatchChange: "chartDocumentPatchChange" }, host: { properties: { "class.praxis-chart-host-fill-container": "isFillContainerMode()", "style.height": "resolvedHeight()", "style.min-height": "resolvedHostMinHeight()", "style.max-height": "resolvedMaxHeight()", "style.aspect-ratio": "resolvedAspectRatio()" } }, viewQueries: [{ propertyName: "chartHost", first: true, predicate: ["chartHost"], descendants: true, isSignal: true }], ngImport: i0, template: `
|
|
3760
4622
|
<section
|
|
3761
4623
|
class="praxis-chart-shell"
|
|
4624
|
+
(keydown)="handleShellKeydown($event)"
|
|
3762
4625
|
[class.praxis-chart-shell-fill-container]="isFillContainerMode()"
|
|
3763
4626
|
[class.praxis-chart-shell-contained]="surfaceMode() === 'contained'"
|
|
3764
4627
|
[class.praxis-chart-shell-embedded]="surfaceMode() === 'embedded'"
|
|
@@ -3929,6 +4792,8 @@ class PraxisChartComponent {
|
|
|
3929
4792
|
class="praxis-chart-point-action"
|
|
3930
4793
|
[attr.aria-label]="accessiblePointActionLabel(point)"
|
|
3931
4794
|
[attr.aria-pressed]="accessiblePointPressed(point)"
|
|
4795
|
+
[praxisQuickView]="accessiblePointQuickViewBinding(point)"
|
|
4796
|
+
[praxisQuickViewRecord]="accessiblePointQuickViewRecord(point)"
|
|
3932
4797
|
(click)="activateAccessiblePoint(point)"
|
|
3933
4798
|
>
|
|
3934
4799
|
{{ point.categoryLabel }}
|
|
@@ -3954,7 +4819,7 @@ class PraxisChartComponent {
|
|
|
3954
4819
|
></div>
|
|
3955
4820
|
}
|
|
3956
4821
|
</section>
|
|
3957
|
-
`, 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){color:inherit}: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{box-sizing:border-box;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,.praxis-chart-data-trigger{position:absolute;top:10px;right:10px;z-index:5;--praxis-icon-button-filled-background: color-mix(in srgb, var(--md-sys-color-surface, #fff) 88%, rgba(18, 99, 180, .12));--praxis-icon-button-filled-foreground: var(--md-sys-color-on-surface-variant, #44474f);-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px)}.praxis-chart-data-trigger-with-settings{right:50px}.praxis-chart-accessible-data{position:absolute;inset:0;z-index:4;display:flex;flex-direction:column;overflow:hidden;padding:52px 16px 14px;color:var(--praxis-chart-config-text-color, var(--md-sys-color-on-surface, #1a1b20));background:var(--md-sys-color-surface-container-lowest, #fff)}.praxis-chart-accessible-data--shell-controlled{padding-top:16px}.praxis-chart-accessible-header{flex-shrink:0;margin-bottom:8px}.praxis-chart-accessible-header h3{margin:0;font-size:.95rem;font-weight:600;line-height:1.4}.praxis-chart-table-container{flex:1;min-height:0;overflow-y:auto;overflow-x:auto;overscroll-behavior:contain}.praxis-chart-accessible-data table{width:100%;border-collapse:separate;border-spacing:0;font-size:.875rem}.praxis-chart-accessible-data caption{padding:0 0 8px;text-align:left;color:inherit;opacity:.85;font-size:.8125rem}.praxis-chart-accessible-data thead{position:sticky;top:0;z-index:2}.praxis-chart-accessible-data thead th{font-weight:600;background:var(--praxis-chart-accessible-thead-bg, inherit);box-shadow:0 1px color-mix(in srgb,var(--md-sys-color-outline, #c5c7ce) 44%,transparent)}.praxis-chart-accessible-data th,.praxis-chart-accessible-data td{padding:8px;border-bottom:1px solid color-mix(in srgb,var(--md-sys-color-outline, #c5c7ce) 44%,transparent);text-align:left}.praxis-chart-col-value{text-align:right!important;font-variant-numeric:tabular-nums}.praxis-chart-sort-header{appearance:none;display:inline-flex;align-items:center;gap:4px;padding:2px 4px;border:0;border-radius:4px;background:transparent;color:inherit;font:inherit;font-weight:600;cursor:pointer;transition:background-color .15s ease}.praxis-chart-sort-header:hover{background:color-mix(in srgb,currentColor 8%,transparent)}.praxis-chart-sort-header:focus-visible{outline:2px solid currentColor;outline-offset:1px}.praxis-chart-sort-header--numeric{margin-left:auto}.praxis-chart-sort-indicator{font-size:.75rem;line-height:1;opacity:.85}.praxis-chart-point-action{appearance:none;padding:4px 6px;border:0;border-radius:4px;color:inherit;background:transparent;font:inherit;font-weight:600;text-align:left;text-decoration:none;cursor:pointer;transition:background-color .15s ease}.praxis-chart-point-action:hover{background:color-mix(in srgb,currentColor 8%,transparent)}.praxis-chart-point-action:focus-visible{outline:3px solid currentColor;outline-offset:2px}.praxis-chart-point-action[aria-pressed=true]{color:var(--md-sys-color-on-primary-container, #001b3f);background:var(--md-sys-color-primary-container, #d7e3ff)}.praxis-chart-host{width:100%;height:100%;min-height:inherit}.praxis-chart-host--data-hidden{visibility:hidden;pointer-events:none}.praxis-chart-host--refreshing{opacity:.36;pointer-events:none;transition:opacity .18s ease}.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-loading--overlay{position:absolute;inset:0;z-index:3;background:color-mix(in srgb,var( --praxis-chart-config-surface-bg, var(--praxis-chart-surface-bg, var(--md-sys-color-surface-container-lowest, #fff)) ) 72%,transparent)}.praxis-chart-state-copy{display:grid;gap:6px;justify-items:center}.praxis-chart-state-title{font-size:1rem;font-weight:600;color:var(--praxis-chart-config-text-color, var(--md-sys-color-on-surface, #1a1b20))}.praxis-chart-state-description{font-size:.925rem;color:color-mix(in srgb,var(--praxis-chart-config-text-color, var(--md-sys-color-on-surface-variant, #5a5d67)) 72%,transparent);max-width:36rem}.praxis-chart-loading-hero{width:min(100%,320px);display:grid;gap:14px}.praxis-chart-loading-summary{display:flex;gap:8px;justify-content:center}.praxis-chart-loading-chip,.praxis-chart-loading-bar{border-radius:999px;background:linear-gradient(90deg,#1263b414,#1263b438,#1263b414);background-size:200% 100%;animation:praxis-chart-loading-wave 1.2s ease-in-out infinite}.praxis-chart-loading-chip{display:block;width:104px;height:12px}.praxis-chart-loading-chip--short{width:64px}.praxis-chart-loading-plot{display:grid;grid-template-columns:repeat(5,minmax(0,1fr));align-items:end;gap:10px;height:108px;padding:12px 8px 4px;border-radius:8px;background:color-mix(in srgb,var(--md-sys-color-surface-container, #eef3f8) 72%,transparent);border:1px solid rgba(18,99,180,.08)}.praxis-chart-loading-bar{display:block;width:100%}.praxis-chart-loading-bar--1{height:32%}.praxis-chart-loading-bar--2{height:68%}.praxis-chart-loading-bar--3{height:48%}.praxis-chart-loading-bar--4{height:78%}.praxis-chart-loading-bar--5{height:56%}@keyframes praxis-chart-loading-wave{0%{background-position:100% 0}to{background-position:-100% 0}}@media(prefers-reduced-motion:reduce){.praxis-chart-host--refreshing{transition:none}.praxis-chart-loading-chip,.praxis-chart-loading-bar{animation:none}}\n"], dependencies: [{ kind: "ngmodule", type: MatTooltipModule }, { kind: "directive", type: i1$2.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "component", type: PraxisIconButtonComponent, selector: "button[praxisIconButton]", inputs: ["praxisIconButton", "size", "appearance", "presentation", "pressed", "busy"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
4822
|
+
`, 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){color:inherit}: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{box-sizing:border-box;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,.praxis-chart-data-trigger{position:absolute;top:10px;right:10px;z-index:5;--praxis-icon-button-filled-background: color-mix(in srgb, var(--md-sys-color-surface, #fff) 88%, rgba(18, 99, 180, .12));--praxis-icon-button-filled-foreground: var(--md-sys-color-on-surface-variant, #44474f);-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px)}.praxis-chart-data-trigger-with-settings{right:50px}.praxis-chart-accessible-data{position:absolute;inset:0;z-index:4;display:flex;flex-direction:column;overflow:hidden;padding:52px 16px 14px;color:var(--praxis-chart-config-text-color, var(--md-sys-color-on-surface, #1a1b20));background:var(--md-sys-color-surface-container-lowest, #fff)}.praxis-chart-accessible-data--shell-controlled{padding-top:16px}.praxis-chart-accessible-header{flex-shrink:0;margin-bottom:8px}.praxis-chart-accessible-header h3{margin:0;font-size:.95rem;font-weight:600;line-height:1.4}.praxis-chart-table-container{flex:1;min-height:0;overflow-y:auto;overflow-x:auto;overscroll-behavior:contain}.praxis-chart-accessible-data table{width:100%;border-collapse:separate;border-spacing:0;font-size:.875rem}.praxis-chart-accessible-data caption{padding:0 0 8px;text-align:left;color:inherit;opacity:.85;font-size:.8125rem}.praxis-chart-accessible-data thead{position:sticky;top:0;z-index:2}.praxis-chart-accessible-data thead th{font-weight:600;background:var(--praxis-chart-accessible-thead-bg, inherit);box-shadow:0 1px color-mix(in srgb,var(--md-sys-color-outline, #c5c7ce) 44%,transparent)}.praxis-chart-accessible-data th,.praxis-chart-accessible-data td{padding:8px;border-bottom:1px solid color-mix(in srgb,var(--md-sys-color-outline, #c5c7ce) 44%,transparent);text-align:left}.praxis-chart-col-value{text-align:right!important;font-variant-numeric:tabular-nums}.praxis-chart-sort-header{appearance:none;display:inline-flex;align-items:center;gap:4px;padding:2px 4px;border:0;border-radius:4px;background:transparent;color:inherit;font:inherit;font-weight:600;cursor:pointer;transition:background-color .15s ease}.praxis-chart-sort-header:hover{background:color-mix(in srgb,currentColor 8%,transparent)}.praxis-chart-sort-header:focus-visible{outline:2px solid currentColor;outline-offset:1px}.praxis-chart-sort-header--numeric{margin-left:auto}.praxis-chart-sort-indicator{font-size:.75rem;line-height:1;opacity:.85}.praxis-chart-point-action{appearance:none;padding:4px 6px;border:0;border-radius:4px;color:inherit;background:transparent;font:inherit;font-weight:600;text-align:left;text-decoration:none;cursor:pointer;transition:background-color .15s ease}.praxis-chart-point-action:hover{background:color-mix(in srgb,currentColor 8%,transparent)}.praxis-chart-point-action:focus-visible{outline:3px solid currentColor;outline-offset:2px}.praxis-chart-point-action[aria-pressed=true]{color:var(--md-sys-color-on-primary-container, #001b3f);background:var(--md-sys-color-primary-container, #d7e3ff)}.praxis-chart-host{width:100%;height:100%;min-height:inherit}.praxis-chart-host--data-hidden{visibility:hidden;pointer-events:none}.praxis-chart-host--refreshing{opacity:.36;pointer-events:none;transition:opacity .18s ease}.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-loading--overlay{position:absolute;inset:0;z-index:3;background:color-mix(in srgb,var( --praxis-chart-config-surface-bg, var(--praxis-chart-surface-bg, var(--md-sys-color-surface-container-lowest, #fff)) ) 72%,transparent)}.praxis-chart-state-copy{display:grid;gap:6px;justify-items:center}.praxis-chart-state-title{font-size:1rem;font-weight:600;color:var(--praxis-chart-config-text-color, var(--md-sys-color-on-surface, #1a1b20))}.praxis-chart-state-description{font-size:.925rem;color:color-mix(in srgb,var(--praxis-chart-config-text-color, var(--md-sys-color-on-surface-variant, #5a5d67)) 72%,transparent);max-width:36rem}.praxis-chart-state-loading .praxis-chart-state-title,.praxis-chart-state-loading .praxis-chart-state-description{background-image:var( --pdx-text-shimmer-gradient, linear-gradient( 105deg, var(--praxis-chart-config-text-color, var(--md-sys-color-on-surface-variant, #5a5d67)) 0%, var(--praxis-chart-config-text-color, var(--md-sys-color-on-surface-variant, #5a5d67)) 28%, var(--md-sys-color-primary, #1263b4) 44%, var(--md-sys-color-tertiary, #7c3aed) 56%, var(--praxis-chart-config-text-color, var(--md-sys-color-on-surface-variant, #5a5d67)) 72%, var(--praxis-chart-config-text-color, var(--md-sys-color-on-surface-variant, #5a5d67)) 100% ) );background-size:260% 100%;background-position:200% 50%;-webkit-background-clip:text;background-clip:text;color:transparent;-webkit-text-fill-color:transparent;animation:praxis-chart-text-shimmer var(--pdx-text-shimmer-duration, 2.4s) linear infinite}@keyframes praxis-chart-text-shimmer{0%{background-position:200% 50%}to{background-position:-60% 50%}}.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}}@media(prefers-reduced-motion:reduce),(forced-colors:active),print{.praxis-chart-host--refreshing{transition:none}.praxis-chart-loading-chip,.praxis-chart-loading-bar{animation:none}.praxis-chart-state-loading .praxis-chart-state-title,.praxis-chart-state-loading .praxis-chart-state-description{animation:none;background-image:none;color:var( --pdx-text-shimmer-fallback-color, var(--praxis-chart-config-text-color, var(--md-sys-color-on-surface, #1a1b20)) );-webkit-text-fill-color:currentColor}}@media(forced-colors:active){.praxis-chart-state-loading .praxis-chart-state-title,.praxis-chart-state-loading .praxis-chart-state-description{color:CanvasText!important;-webkit-text-fill-color:CanvasText!important}}\n"], dependencies: [{ kind: "ngmodule", type: MatTooltipModule }, { kind: "directive", type: i1$2.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "component", type: PraxisIconButtonComponent, selector: "button[praxisIconButton]", inputs: ["praxisIconButton", "size", "appearance", "presentation", "pressed", "busy"] }, { kind: "directive", type: PraxisQuickViewTriggerDirective, selector: "[praxisQuickView]", inputs: ["praxisQuickView", "praxisQuickViewRecord", "praxisQuickViewTriggerMode", "praxisQuickViewAnchorId", "praxisQuickViewSourceId", "praxisQuickViewEndpointKey", "praxisQuickViewApiUrlEntry", "praxisQuickViewQueryContext", "praxisQuickViewHoverOpenDelayMs", "praxisQuickViewHoverCloseDelayMs", "praxisQuickViewStopPropagation"], exportAs: ["praxisQuickView"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
3958
4823
|
}
|
|
3959
4824
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: PraxisChartComponent, decorators: [{
|
|
3960
4825
|
type: Component,
|
|
@@ -3964,9 +4829,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
|
|
|
3964
4829
|
'[style.min-height]': 'resolvedHostMinHeight()',
|
|
3965
4830
|
'[style.max-height]': 'resolvedMaxHeight()',
|
|
3966
4831
|
'[style.aspect-ratio]': 'resolvedAspectRatio()',
|
|
3967
|
-
}, imports: [MatTooltipModule, PraxisIconButtonComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: `
|
|
4832
|
+
}, imports: [MatTooltipModule, PraxisIconButtonComponent, PraxisQuickViewTriggerDirective], changeDetection: ChangeDetectionStrategy.OnPush, template: `
|
|
3968
4833
|
<section
|
|
3969
4834
|
class="praxis-chart-shell"
|
|
4835
|
+
(keydown)="handleShellKeydown($event)"
|
|
3970
4836
|
[class.praxis-chart-shell-fill-container]="isFillContainerMode()"
|
|
3971
4837
|
[class.praxis-chart-shell-contained]="surfaceMode() === 'contained'"
|
|
3972
4838
|
[class.praxis-chart-shell-embedded]="surfaceMode() === 'embedded'"
|
|
@@ -4137,6 +5003,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
|
|
|
4137
5003
|
class="praxis-chart-point-action"
|
|
4138
5004
|
[attr.aria-label]="accessiblePointActionLabel(point)"
|
|
4139
5005
|
[attr.aria-pressed]="accessiblePointPressed(point)"
|
|
5006
|
+
[praxisQuickView]="accessiblePointQuickViewBinding(point)"
|
|
5007
|
+
[praxisQuickViewRecord]="accessiblePointQuickViewRecord(point)"
|
|
4140
5008
|
(click)="activateAccessiblePoint(point)"
|
|
4141
5009
|
>
|
|
4142
5010
|
{{ point.categoryLabel }}
|
|
@@ -4162,7 +5030,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
|
|
|
4162
5030
|
></div>
|
|
4163
5031
|
}
|
|
4164
5032
|
</section>
|
|
4165
|
-
`, 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){color:inherit}: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{box-sizing:border-box;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,.praxis-chart-data-trigger{position:absolute;top:10px;right:10px;z-index:5;--praxis-icon-button-filled-background: color-mix(in srgb, var(--md-sys-color-surface, #fff) 88%, rgba(18, 99, 180, .12));--praxis-icon-button-filled-foreground: var(--md-sys-color-on-surface-variant, #44474f);-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px)}.praxis-chart-data-trigger-with-settings{right:50px}.praxis-chart-accessible-data{position:absolute;inset:0;z-index:4;display:flex;flex-direction:column;overflow:hidden;padding:52px 16px 14px;color:var(--praxis-chart-config-text-color, var(--md-sys-color-on-surface, #1a1b20));background:var(--md-sys-color-surface-container-lowest, #fff)}.praxis-chart-accessible-data--shell-controlled{padding-top:16px}.praxis-chart-accessible-header{flex-shrink:0;margin-bottom:8px}.praxis-chart-accessible-header h3{margin:0;font-size:.95rem;font-weight:600;line-height:1.4}.praxis-chart-table-container{flex:1;min-height:0;overflow-y:auto;overflow-x:auto;overscroll-behavior:contain}.praxis-chart-accessible-data table{width:100%;border-collapse:separate;border-spacing:0;font-size:.875rem}.praxis-chart-accessible-data caption{padding:0 0 8px;text-align:left;color:inherit;opacity:.85;font-size:.8125rem}.praxis-chart-accessible-data thead{position:sticky;top:0;z-index:2}.praxis-chart-accessible-data thead th{font-weight:600;background:var(--praxis-chart-accessible-thead-bg, inherit);box-shadow:0 1px color-mix(in srgb,var(--md-sys-color-outline, #c5c7ce) 44%,transparent)}.praxis-chart-accessible-data th,.praxis-chart-accessible-data td{padding:8px;border-bottom:1px solid color-mix(in srgb,var(--md-sys-color-outline, #c5c7ce) 44%,transparent);text-align:left}.praxis-chart-col-value{text-align:right!important;font-variant-numeric:tabular-nums}.praxis-chart-sort-header{appearance:none;display:inline-flex;align-items:center;gap:4px;padding:2px 4px;border:0;border-radius:4px;background:transparent;color:inherit;font:inherit;font-weight:600;cursor:pointer;transition:background-color .15s ease}.praxis-chart-sort-header:hover{background:color-mix(in srgb,currentColor 8%,transparent)}.praxis-chart-sort-header:focus-visible{outline:2px solid currentColor;outline-offset:1px}.praxis-chart-sort-header--numeric{margin-left:auto}.praxis-chart-sort-indicator{font-size:.75rem;line-height:1;opacity:.85}.praxis-chart-point-action{appearance:none;padding:4px 6px;border:0;border-radius:4px;color:inherit;background:transparent;font:inherit;font-weight:600;text-align:left;text-decoration:none;cursor:pointer;transition:background-color .15s ease}.praxis-chart-point-action:hover{background:color-mix(in srgb,currentColor 8%,transparent)}.praxis-chart-point-action:focus-visible{outline:3px solid currentColor;outline-offset:2px}.praxis-chart-point-action[aria-pressed=true]{color:var(--md-sys-color-on-primary-container, #001b3f);background:var(--md-sys-color-primary-container, #d7e3ff)}.praxis-chart-host{width:100%;height:100%;min-height:inherit}.praxis-chart-host--data-hidden{visibility:hidden;pointer-events:none}.praxis-chart-host--refreshing{opacity:.36;pointer-events:none;transition:opacity .18s ease}.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-loading--overlay{position:absolute;inset:0;z-index:3;background:color-mix(in srgb,var( --praxis-chart-config-surface-bg, var(--praxis-chart-surface-bg, var(--md-sys-color-surface-container-lowest, #fff)) ) 72%,transparent)}.praxis-chart-state-copy{display:grid;gap:6px;justify-items:center}.praxis-chart-state-title{font-size:1rem;font-weight:600;color:var(--praxis-chart-config-text-color, var(--md-sys-color-on-surface, #1a1b20))}.praxis-chart-state-description{font-size:.925rem;color:color-mix(in srgb,var(--praxis-chart-config-text-color, var(--md-sys-color-on-surface-variant, #5a5d67)) 72%,transparent);max-width:36rem}.praxis-chart-loading-hero{width:min(100%,320px);display:grid;gap:14px}.praxis-chart-loading-summary{display:flex;gap:8px;justify-content:center}.praxis-chart-loading-chip,.praxis-chart-loading-bar{border-radius:999px;background:linear-gradient(90deg,#1263b414,#1263b438,#1263b414);background-size:200% 100%;animation:praxis-chart-loading-wave 1.2s ease-in-out infinite}.praxis-chart-loading-chip{display:block;width:104px;height:12px}.praxis-chart-loading-chip--short{width:64px}.praxis-chart-loading-plot{display:grid;grid-template-columns:repeat(5,minmax(0,1fr));align-items:end;gap:10px;height:108px;padding:12px 8px 4px;border-radius:8px;background:color-mix(in srgb,var(--md-sys-color-surface-container, #eef3f8) 72%,transparent);border:1px solid rgba(18,99,180,.08)}.praxis-chart-loading-bar{display:block;width:100%}.praxis-chart-loading-bar--1{height:32%}.praxis-chart-loading-bar--2{height:68%}.praxis-chart-loading-bar--3{height:48%}.praxis-chart-loading-bar--4{height:78%}.praxis-chart-loading-bar--5{height:56%}@keyframes praxis-chart-loading-wave{0%{background-position:100% 0}to{background-position:-100% 0}}@media(prefers-reduced-motion:reduce){.praxis-chart-host--refreshing{transition:none}.praxis-chart-loading-chip,.praxis-chart-loading-bar{animation:none}}\n"] }]
|
|
5033
|
+
`, 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){color:inherit}: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{box-sizing:border-box;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,.praxis-chart-data-trigger{position:absolute;top:10px;right:10px;z-index:5;--praxis-icon-button-filled-background: color-mix(in srgb, var(--md-sys-color-surface, #fff) 88%, rgba(18, 99, 180, .12));--praxis-icon-button-filled-foreground: var(--md-sys-color-on-surface-variant, #44474f);-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px)}.praxis-chart-data-trigger-with-settings{right:50px}.praxis-chart-accessible-data{position:absolute;inset:0;z-index:4;display:flex;flex-direction:column;overflow:hidden;padding:52px 16px 14px;color:var(--praxis-chart-config-text-color, var(--md-sys-color-on-surface, #1a1b20));background:var(--md-sys-color-surface-container-lowest, #fff)}.praxis-chart-accessible-data--shell-controlled{padding-top:16px}.praxis-chart-accessible-header{flex-shrink:0;margin-bottom:8px}.praxis-chart-accessible-header h3{margin:0;font-size:.95rem;font-weight:600;line-height:1.4}.praxis-chart-table-container{flex:1;min-height:0;overflow-y:auto;overflow-x:auto;overscroll-behavior:contain}.praxis-chart-accessible-data table{width:100%;border-collapse:separate;border-spacing:0;font-size:.875rem}.praxis-chart-accessible-data caption{padding:0 0 8px;text-align:left;color:inherit;opacity:.85;font-size:.8125rem}.praxis-chart-accessible-data thead{position:sticky;top:0;z-index:2}.praxis-chart-accessible-data thead th{font-weight:600;background:var(--praxis-chart-accessible-thead-bg, inherit);box-shadow:0 1px color-mix(in srgb,var(--md-sys-color-outline, #c5c7ce) 44%,transparent)}.praxis-chart-accessible-data th,.praxis-chart-accessible-data td{padding:8px;border-bottom:1px solid color-mix(in srgb,var(--md-sys-color-outline, #c5c7ce) 44%,transparent);text-align:left}.praxis-chart-col-value{text-align:right!important;font-variant-numeric:tabular-nums}.praxis-chart-sort-header{appearance:none;display:inline-flex;align-items:center;gap:4px;padding:2px 4px;border:0;border-radius:4px;background:transparent;color:inherit;font:inherit;font-weight:600;cursor:pointer;transition:background-color .15s ease}.praxis-chart-sort-header:hover{background:color-mix(in srgb,currentColor 8%,transparent)}.praxis-chart-sort-header:focus-visible{outline:2px solid currentColor;outline-offset:1px}.praxis-chart-sort-header--numeric{margin-left:auto}.praxis-chart-sort-indicator{font-size:.75rem;line-height:1;opacity:.85}.praxis-chart-point-action{appearance:none;padding:4px 6px;border:0;border-radius:4px;color:inherit;background:transparent;font:inherit;font-weight:600;text-align:left;text-decoration:none;cursor:pointer;transition:background-color .15s ease}.praxis-chart-point-action:hover{background:color-mix(in srgb,currentColor 8%,transparent)}.praxis-chart-point-action:focus-visible{outline:3px solid currentColor;outline-offset:2px}.praxis-chart-point-action[aria-pressed=true]{color:var(--md-sys-color-on-primary-container, #001b3f);background:var(--md-sys-color-primary-container, #d7e3ff)}.praxis-chart-host{width:100%;height:100%;min-height:inherit}.praxis-chart-host--data-hidden{visibility:hidden;pointer-events:none}.praxis-chart-host--refreshing{opacity:.36;pointer-events:none;transition:opacity .18s ease}.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-loading--overlay{position:absolute;inset:0;z-index:3;background:color-mix(in srgb,var( --praxis-chart-config-surface-bg, var(--praxis-chart-surface-bg, var(--md-sys-color-surface-container-lowest, #fff)) ) 72%,transparent)}.praxis-chart-state-copy{display:grid;gap:6px;justify-items:center}.praxis-chart-state-title{font-size:1rem;font-weight:600;color:var(--praxis-chart-config-text-color, var(--md-sys-color-on-surface, #1a1b20))}.praxis-chart-state-description{font-size:.925rem;color:color-mix(in srgb,var(--praxis-chart-config-text-color, var(--md-sys-color-on-surface-variant, #5a5d67)) 72%,transparent);max-width:36rem}.praxis-chart-state-loading .praxis-chart-state-title,.praxis-chart-state-loading .praxis-chart-state-description{background-image:var( --pdx-text-shimmer-gradient, linear-gradient( 105deg, var(--praxis-chart-config-text-color, var(--md-sys-color-on-surface-variant, #5a5d67)) 0%, var(--praxis-chart-config-text-color, var(--md-sys-color-on-surface-variant, #5a5d67)) 28%, var(--md-sys-color-primary, #1263b4) 44%, var(--md-sys-color-tertiary, #7c3aed) 56%, var(--praxis-chart-config-text-color, var(--md-sys-color-on-surface-variant, #5a5d67)) 72%, var(--praxis-chart-config-text-color, var(--md-sys-color-on-surface-variant, #5a5d67)) 100% ) );background-size:260% 100%;background-position:200% 50%;-webkit-background-clip:text;background-clip:text;color:transparent;-webkit-text-fill-color:transparent;animation:praxis-chart-text-shimmer var(--pdx-text-shimmer-duration, 2.4s) linear infinite}@keyframes praxis-chart-text-shimmer{0%{background-position:200% 50%}to{background-position:-60% 50%}}.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}}@media(prefers-reduced-motion:reduce),(forced-colors:active),print{.praxis-chart-host--refreshing{transition:none}.praxis-chart-loading-chip,.praxis-chart-loading-bar{animation:none}.praxis-chart-state-loading .praxis-chart-state-title,.praxis-chart-state-loading .praxis-chart-state-description{animation:none;background-image:none;color:var( --pdx-text-shimmer-fallback-color, var(--praxis-chart-config-text-color, var(--md-sys-color-on-surface, #1a1b20)) );-webkit-text-fill-color:currentColor}}@media(forced-colors:active){.praxis-chart-state-loading .praxis-chart-state-title,.praxis-chart-state-loading .praxis-chart-state-description{color:CanvasText!important;-webkit-text-fill-color:CanvasText!important}}\n"] }]
|
|
4166
5034
|
}], ctorParameters: () => [], propDecorators: { config: [{ type: i0.Input, args: [{ isSignal: true, alias: "config", required: false }] }], resourceKey: [{ type: i0.Input, args: [{ isSignal: true, alias: "resourceKey", 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"] }], chartDocumentPatchChange: [{ type: i0.Output, args: ["chartDocumentPatchChange"] }], chartHost: [{ type: i0.ViewChild, args: ['chartHost', { isSignal: true }] }] } });
|
|
4167
5035
|
function mergeRemoteQueryContext(config, queryContext, runtimeFilters) {
|
|
4168
5036
|
const dataSource = config.dataSource;
|
|
@@ -4244,6 +5112,24 @@ function buildAccessibleChartPoints(config, transformed, formatValue, pointFallb
|
|
|
4244
5112
|
seriesFallback,
|
|
4245
5113
|
}));
|
|
4246
5114
|
}
|
|
5115
|
+
if (transformed.mode === 'heatmap' && transformed.heatmap) {
|
|
5116
|
+
const heatmap = transformed.heatmap;
|
|
5117
|
+
return heatmap.cells
|
|
5118
|
+
.filter((cell) => cell.rawValue !== null && cell.rawValue !== undefined)
|
|
5119
|
+
.map((cell, index) => createAccessiblePoint({
|
|
5120
|
+
id: cell.id,
|
|
5121
|
+
config,
|
|
5122
|
+
seriesId: heatmap.id,
|
|
5123
|
+
seriesName: `${cell.yCategory} (${heatmap.name})`,
|
|
5124
|
+
category: cell.xCategory,
|
|
5125
|
+
value: cell.rawValue,
|
|
5126
|
+
data: cell.data,
|
|
5127
|
+
index,
|
|
5128
|
+
formatValue,
|
|
5129
|
+
pointFallback,
|
|
5130
|
+
seriesFallback,
|
|
5131
|
+
}));
|
|
5132
|
+
}
|
|
4247
5133
|
return transformed.series.flatMap((series) => series.points.flatMap((point, index) => {
|
|
4248
5134
|
const value = accessiblePointValue(point);
|
|
4249
5135
|
if (value === null || value === undefined) {
|
|
@@ -4291,7 +5177,7 @@ function createAccessiblePoint(options) {
|
|
|
4291
5177
|
}
|
|
4292
5178
|
function accessiblePointValue(point) {
|
|
4293
5179
|
if (Array.isArray(point)) {
|
|
4294
|
-
return point.length > 1 ? point[1] : point[0];
|
|
5180
|
+
return point.length > 1 ? point[point.length - 1] : point[0];
|
|
4295
5181
|
}
|
|
4296
5182
|
if (point && typeof point === 'object' && 'value' in point) {
|
|
4297
5183
|
return accessiblePointValue(point['value']);
|
|
@@ -5915,7 +6801,8 @@ class AnalyticsChartConfigAdapterService {
|
|
|
5915
6801
|
toPraxisChartConfig(projection, options) {
|
|
5916
6802
|
const chartType = this.resolveChartType(projection);
|
|
5917
6803
|
const orientation = chartType === 'horizontal-bar' ? 'horizontal' : undefined;
|
|
5918
|
-
const
|
|
6804
|
+
const dimensions = this.statsBuilder.resolveDimensionBindings(projection);
|
|
6805
|
+
const dimension = dimensions[0];
|
|
5919
6806
|
const metrics = projection.bindings.primaryMetrics ?? [];
|
|
5920
6807
|
if (!dimension?.field) {
|
|
5921
6808
|
throw new Error(`AnalyticsChartConfigAdapterService requires primaryDimension for projection "${projection.id}".`);
|
|
@@ -5924,10 +6811,23 @@ class AnalyticsChartConfigAdapterService {
|
|
|
5924
6811
|
throw new Error(`AnalyticsChartConfigAdapterService requires at least one metric for projection "${projection.id}".`);
|
|
5925
6812
|
}
|
|
5926
6813
|
const crossFilter = Boolean(projection.interactions?.crossFilter);
|
|
6814
|
+
const pointSelection = Boolean(projection.interactions?.pointSelection);
|
|
6815
|
+
const drillDown = Boolean(projection.interactions?.drillDown);
|
|
5927
6816
|
const keyFilterField = dimension.keyFilterField?.trim();
|
|
6817
|
+
const secondaryKeyFilterField = dimensions[1]?.keyFilterField?.trim();
|
|
5928
6818
|
if (crossFilter && !keyFilterField) {
|
|
5929
6819
|
throw new Error(`AnalyticsChartConfigAdapterService requires primaryDimension.keyFilterField when crossFilter is enabled for projection "${projection.id}".`);
|
|
5930
6820
|
}
|
|
6821
|
+
if (crossFilter && dimensions[1] && dimensions[1].keyFilterField !== undefined && !secondaryKeyFilterField) {
|
|
6822
|
+
throw new Error(`AnalyticsChartConfigAdapterService requires secondaryDimension.keyFilterField to be non-empty when crossFilter is enabled for projection "${projection.id}".`);
|
|
6823
|
+
}
|
|
6824
|
+
const keyFilterMapping = keyFilterField
|
|
6825
|
+
? {
|
|
6826
|
+
key: keyFilterField,
|
|
6827
|
+
...(secondaryKeyFilterField ? { secondaryKey: secondaryKeyFilterField } : {}),
|
|
6828
|
+
}
|
|
6829
|
+
: undefined;
|
|
6830
|
+
const hasMappedEventActions = Boolean(keyFilterMapping && (crossFilter || pointSelection || drillDown));
|
|
5931
6831
|
return {
|
|
5932
6832
|
id: projection.id,
|
|
5933
6833
|
type: chartType,
|
|
@@ -5940,17 +6840,37 @@ class AnalyticsChartConfigAdapterService {
|
|
|
5940
6840
|
series: this.buildSeries(projection, chartType),
|
|
5941
6841
|
dataSource: this.buildDataSource(projection),
|
|
5942
6842
|
interactions: {
|
|
5943
|
-
pointClick: Boolean(
|
|
5944
|
-
drillDown
|
|
5945
|
-
selection:
|
|
6843
|
+
pointClick: Boolean(pointSelection || drillDown),
|
|
6844
|
+
drillDown,
|
|
6845
|
+
selection: pointSelection,
|
|
5946
6846
|
crossFilter,
|
|
5947
|
-
...(
|
|
6847
|
+
...(hasMappedEventActions
|
|
5948
6848
|
? {
|
|
5949
6849
|
eventActions: {
|
|
5950
|
-
crossFilter
|
|
5951
|
-
|
|
5952
|
-
|
|
5953
|
-
|
|
6850
|
+
...(crossFilter
|
|
6851
|
+
? {
|
|
6852
|
+
crossFilter: {
|
|
6853
|
+
action: 'emit',
|
|
6854
|
+
mapping: { ...keyFilterMapping },
|
|
6855
|
+
},
|
|
6856
|
+
}
|
|
6857
|
+
: {}),
|
|
6858
|
+
...(pointSelection
|
|
6859
|
+
? {
|
|
6860
|
+
selectionChange: {
|
|
6861
|
+
action: 'emit',
|
|
6862
|
+
mapping: { ...keyFilterMapping },
|
|
6863
|
+
},
|
|
6864
|
+
}
|
|
6865
|
+
: {}),
|
|
6866
|
+
...(drillDown
|
|
6867
|
+
? {
|
|
6868
|
+
drillDown: {
|
|
6869
|
+
action: 'emit',
|
|
6870
|
+
mapping: { ...keyFilterMapping },
|
|
6871
|
+
},
|
|
6872
|
+
}
|
|
6873
|
+
: {}),
|
|
5954
6874
|
},
|
|
5955
6875
|
}
|
|
5956
6876
|
: {}),
|
|
@@ -5958,20 +6878,40 @@ class AnalyticsChartConfigAdapterService {
|
|
|
5958
6878
|
};
|
|
5959
6879
|
}
|
|
5960
6880
|
buildAxes(projection, chartType) {
|
|
5961
|
-
const
|
|
6881
|
+
const dimensions = this.statsBuilder.resolveDimensionBindings(projection);
|
|
6882
|
+
const dimension = dimensions[0];
|
|
6883
|
+
const secondaryDimension = dimensions[1];
|
|
6884
|
+
const primaryField = this.statsBuilder.resolveEffectiveDimensionField(dimension, dimensions);
|
|
6885
|
+
const secondaryField = secondaryDimension
|
|
6886
|
+
? this.statsBuilder.resolveEffectiveDimensionField(secondaryDimension, dimensions)
|
|
6887
|
+
: undefined;
|
|
5962
6888
|
const metrics = this.getDisplayMetrics(projection);
|
|
5963
6889
|
const firstMetric = metrics[0];
|
|
5964
6890
|
if (this.isCategorySliceChart(chartType)) {
|
|
5965
6891
|
return {
|
|
5966
6892
|
x: {
|
|
5967
|
-
field:
|
|
6893
|
+
field: primaryField,
|
|
5968
6894
|
label: dimension.label ?? undefined,
|
|
5969
6895
|
},
|
|
5970
6896
|
};
|
|
5971
6897
|
}
|
|
6898
|
+
if (chartType === 'heatmap') {
|
|
6899
|
+
return {
|
|
6900
|
+
x: {
|
|
6901
|
+
field: primaryField,
|
|
6902
|
+
label: dimension.label ?? undefined,
|
|
6903
|
+
type: 'category',
|
|
6904
|
+
},
|
|
6905
|
+
y: {
|
|
6906
|
+
field: secondaryField,
|
|
6907
|
+
label: secondaryDimension?.label ?? undefined,
|
|
6908
|
+
type: 'category',
|
|
6909
|
+
},
|
|
6910
|
+
};
|
|
6911
|
+
}
|
|
5972
6912
|
return {
|
|
5973
6913
|
x: {
|
|
5974
|
-
field:
|
|
6914
|
+
field: primaryField,
|
|
5975
6915
|
label: dimension.label ?? undefined,
|
|
5976
6916
|
type: dimension.role === 'time' ? 'time' : 'category',
|
|
5977
6917
|
},
|
|
@@ -5984,31 +6924,42 @@ class AnalyticsChartConfigAdapterService {
|
|
|
5984
6924
|
};
|
|
5985
6925
|
}
|
|
5986
6926
|
buildSeries(projection, chartType) {
|
|
5987
|
-
const
|
|
6927
|
+
const dimensions = this.statsBuilder.resolveDimensionBindings(projection);
|
|
6928
|
+
const dimension = dimensions[0];
|
|
6929
|
+
const secondaryDimension = dimensions[1];
|
|
6930
|
+
const primaryField = this.statsBuilder.resolveEffectiveDimensionField(dimension, dimensions);
|
|
6931
|
+
const secondaryField = secondaryDimension
|
|
6932
|
+
? this.statsBuilder.resolveEffectiveDimensionField(secondaryDimension, dimensions)
|
|
6933
|
+
: undefined;
|
|
5988
6934
|
const comparison = projection.source.operation === 'comparison';
|
|
5989
6935
|
return this.getDisplayMetrics(projection).flatMap((metric, index) => comparison
|
|
5990
6936
|
? ['current', 'previous'].map((period) => ({
|
|
5991
6937
|
id: `${projection.id}.${metric.field}.${period}`,
|
|
5992
6938
|
name: `${metric.label ?? metric.field} (${this.comparisonPeriodLabel(period)})`,
|
|
5993
6939
|
type: chartType,
|
|
5994
|
-
categoryField:
|
|
6940
|
+
categoryField: primaryField,
|
|
5995
6941
|
metric: { field: this.comparisonMetricField(metric.field, period), aggregation: this.mapAggregation(metric.aggregation) },
|
|
5996
6942
|
}))
|
|
5997
6943
|
: [{
|
|
5998
6944
|
id: `${projection.id}.${metric.field}.${index + 1}`,
|
|
5999
6945
|
name: metric.label ?? metric.field,
|
|
6000
|
-
type: chartType,
|
|
6001
|
-
categoryField: this.isCategorySliceChart(chartType) ?
|
|
6946
|
+
type: chartType === 'stacked-bar' ? 'bar' : chartType,
|
|
6947
|
+
categoryField: this.isCategorySliceChart(chartType) ? primaryField : undefined,
|
|
6948
|
+
...(secondaryField ? { secondaryCategoryField: secondaryField } : {}),
|
|
6949
|
+
...(chartType === 'stacked-bar' || chartType === 'stacked-area' ? { stackId: 'stack-1' } : {}),
|
|
6002
6950
|
metric: {
|
|
6003
6951
|
field: metric.field,
|
|
6004
6952
|
aggregation: this.mapAggregation(metric.aggregation),
|
|
6005
6953
|
label: metric.label ?? undefined,
|
|
6006
6954
|
},
|
|
6007
|
-
smooth: chartType === 'line' || chartType === 'area',
|
|
6955
|
+
smooth: chartType === 'line' || chartType === 'area' || chartType === 'stacked-area',
|
|
6008
6956
|
}]);
|
|
6009
6957
|
}
|
|
6010
6958
|
buildDataSource(projection) {
|
|
6011
6959
|
const executionPlan = this.statsBuilder.buildExecutionPlan(projection);
|
|
6960
|
+
const groupByRequest = executionPlan.operation === 'group-by'
|
|
6961
|
+
? executionPlan.statsRequest
|
|
6962
|
+
: undefined;
|
|
6012
6963
|
return {
|
|
6013
6964
|
kind: 'remote',
|
|
6014
6965
|
resourcePath: executionPlan.resourcePath,
|
|
@@ -6016,9 +6967,14 @@ class AnalyticsChartConfigAdapterService {
|
|
|
6016
6967
|
query: {
|
|
6017
6968
|
sourceKind: 'praxis.stats',
|
|
6018
6969
|
statsOperation: executionPlan.operation,
|
|
6970
|
+
...(groupByRequest?.fillGaps !== undefined ? { fillGaps: groupByRequest.fillGaps } : {}),
|
|
6971
|
+
...(groupByRequest?.timezone ? { timezone: groupByRequest.timezone } : {}),
|
|
6972
|
+
...(groupByRequest?.periodField ? { periodField: groupByRequest.periodField } : {}),
|
|
6973
|
+
...(groupByRequest?.periodPreset ? { periodPreset: groupByRequest.periodPreset } : {}),
|
|
6019
6974
|
statsPath: executionPlan.statsPath,
|
|
6020
6975
|
statsRequest: executionPlan.statsRequest,
|
|
6021
6976
|
dimensions: executionPlan.dimensions,
|
|
6977
|
+
...(groupByRequest?.dimensions?.length ? { dimensionConfigs: groupByRequest.dimensions } : {}),
|
|
6022
6978
|
metrics: projection.source.operation === 'comparison'
|
|
6023
6979
|
? this.getDisplayMetrics(projection).flatMap((metric) => ['current', 'previous'].map((period) => ({
|
|
6024
6980
|
field: this.comparisonMetricField(metric.field, period),
|
|
@@ -6032,6 +6988,7 @@ class AnalyticsChartConfigAdapterService {
|
|
|
6032
6988
|
};
|
|
6033
6989
|
}
|
|
6034
6990
|
resolveChartType(projection) {
|
|
6991
|
+
const dimensions = this.statsBuilder.resolveDimensionBindings(projection);
|
|
6035
6992
|
if (projection.intent === 'trend' && projection.source.operation === 'timeseries') {
|
|
6036
6993
|
return 'line';
|
|
6037
6994
|
}
|
|
@@ -6039,10 +6996,10 @@ class AnalyticsChartConfigAdapterService {
|
|
|
6039
6996
|
return 'horizontal-bar';
|
|
6040
6997
|
}
|
|
6041
6998
|
if (projection.intent === 'composition') {
|
|
6042
|
-
return 'pie';
|
|
6999
|
+
return dimensions.length > 1 ? 'stacked-bar' : 'pie';
|
|
6043
7000
|
}
|
|
6044
7001
|
if (projection.intent === 'distribution') {
|
|
6045
|
-
return 'bar';
|
|
7002
|
+
return dimensions.length > 1 ? 'heatmap' : 'bar';
|
|
6046
7003
|
}
|
|
6047
7004
|
return 'bar';
|
|
6048
7005
|
}
|
|
@@ -6062,7 +7019,10 @@ class AnalyticsChartConfigAdapterService {
|
|
|
6062
7019
|
case 'count':
|
|
6063
7020
|
case 'distinct-count':
|
|
6064
7021
|
case 'sum':
|
|
7022
|
+
case 'avg-per-cycle-occurrence':
|
|
6065
7023
|
return normalizedAggregation;
|
|
7024
|
+
case 'avg_per_cycle_occurrence':
|
|
7025
|
+
return 'avg-per-cycle-occurrence';
|
|
6066
7026
|
case '':
|
|
6067
7027
|
return undefined;
|
|
6068
7028
|
default:
|
|
@@ -6123,6 +7083,7 @@ class PraxisAnalyticsPresentationComponent {
|
|
|
6123
7083
|
queryRequest = output();
|
|
6124
7084
|
loadStateChange = output();
|
|
6125
7085
|
resolver = inject(AnalyticsPresentationResolver);
|
|
7086
|
+
statsBuilder = inject(AnalyticsStatsRequestBuilderService);
|
|
6126
7087
|
i18n = inject(PraxisI18nService);
|
|
6127
7088
|
chartAdapter = inject(AnalyticsChartConfigAdapterService);
|
|
6128
7089
|
tableAdapter = inject(AnalyticsTableConfigAdapterService);
|
|
@@ -6160,15 +7121,17 @@ class PraxisAnalyticsPresentationComponent {
|
|
|
6160
7121
|
kpiSubtitle = computed(() => this.resolveText(this.subtitle()), ...(ngDevMode ? [{ debugName: "kpiSubtitle" }] : /* istanbul ignore next */ []));
|
|
6161
7122
|
kpiItems = computed(() => {
|
|
6162
7123
|
const projection = this.resolved().projection;
|
|
6163
|
-
const
|
|
7124
|
+
const dimensions = projection ? this.statsBuilder.resolveDimensionBindings(projection) : [];
|
|
7125
|
+
const dimension = dimensions[0];
|
|
6164
7126
|
const metric = projection?.bindings.primaryMetrics[0];
|
|
6165
7127
|
if (!projection || !dimension?.field || !metric?.field || !this.isKpiEligible(projection)) {
|
|
6166
7128
|
return [];
|
|
6167
7129
|
}
|
|
7130
|
+
const effectiveDimField = this.statsBuilder.resolveEffectiveDimensionField(dimension, dimensions);
|
|
6168
7131
|
return this.resultRows().map((row, index) => {
|
|
6169
7132
|
const rawValue = row[metric.field];
|
|
6170
7133
|
const value = typeof rawValue === 'number' && Number.isFinite(rawValue) ? rawValue : 0;
|
|
6171
|
-
const label = stringifyValue(row[
|
|
7134
|
+
const label = stringifyValue(row[effectiveDimField]) ?? this.t('praxis.charts.analyticsPresentation.kpiUnknownBucket', 'Unidentified category');
|
|
6172
7135
|
return {
|
|
6173
7136
|
trackId: `${stringifyValue(row[ANALYTICS_TABLE_ROW_KEY_FIELD]) ?? label}-${index}`,
|
|
6174
7137
|
label,
|
|
@@ -6240,7 +7203,10 @@ class PraxisAnalyticsPresentationComponent {
|
|
|
6240
7203
|
const projection = this.resolved().projection;
|
|
6241
7204
|
if (!row || !projection)
|
|
6242
7205
|
return;
|
|
6243
|
-
const
|
|
7206
|
+
const dimensions = this.statsBuilder.resolveDimensionBindings(projection);
|
|
7207
|
+
const dimensionField = dimensions[0]
|
|
7208
|
+
? this.statsBuilder.resolveEffectiveDimensionField(dimensions[0], dimensions)
|
|
7209
|
+
: undefined;
|
|
6244
7210
|
const metricField = projection.bindings.primaryMetrics[0]?.field;
|
|
6245
7211
|
this.pointClick.emit({
|
|
6246
7212
|
chartId: projection.id,
|
|
@@ -6318,8 +7284,10 @@ class PraxisAnalyticsPresentationComponent {
|
|
|
6318
7284
|
}
|
|
6319
7285
|
}
|
|
6320
7286
|
isKpiEligible(projection) {
|
|
7287
|
+
const dimensions = this.statsBuilder.resolveDimensionBindings(projection);
|
|
6321
7288
|
return (projection.source.operation === 'group-by'
|
|
6322
|
-
&&
|
|
7289
|
+
&& dimensions.length === 1
|
|
7290
|
+
&& !!dimensions[0]?.field
|
|
6323
7291
|
&& projection.bindings.primaryMetrics.length === 1
|
|
6324
7292
|
&& !!projection.bindings.primaryMetrics[0]?.field
|
|
6325
7293
|
&& (projection.presentationHints?.preferredFamilies ?? []).includes('kpi'));
|
|
@@ -6340,11 +7308,11 @@ class PraxisAnalyticsPresentationComponent {
|
|
|
6340
7308
|
return Object.entries(values).reduce((message, [name, value]) => message.replaceAll(`{${name}}`, value), this.t(key, fallback));
|
|
6341
7309
|
}
|
|
6342
7310
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: PraxisAnalyticsPresentationComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
6343
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.14", type: PraxisAnalyticsPresentationComponent, isStandalone: true, selector: "praxis-analytics-presentation", inputs: { analytics: { classPropertyName: "analytics", publicName: "analytics", isSignal: true, isRequired: true, transformFunction: null }, projectionId: { classPropertyName: "projectionId", publicName: "projectionId", isSignal: true, isRequired: true, transformFunction: null }, availableFamilies: { classPropertyName: "availableFamilies", publicName: "availableFamilies", isSignal: true, isRequired: false, transformFunction: null }, preferredFamily: { classPropertyName: "preferredFamily", publicName: "preferredFamily", isSignal: true, isRequired: false, transformFunction: null }, queryContext: { classPropertyName: "queryContext", publicName: "queryContext", isSignal: true, isRequired: false, transformFunction: null }, title: { classPropertyName: "title", publicName: "title", isSignal: true, isRequired: false, transformFunction: null }, subtitle: { classPropertyName: "subtitle", publicName: "subtitle", isSignal: true, isRequired: false, transformFunction: null }, height: { classPropertyName: "height", publicName: "height", isSignal: true, isRequired: false, transformFunction: null }, motion: { classPropertyName: "motion", publicName: "motion", isSignal: true, isRequired: false, transformFunction: null }, enableCustomization: { classPropertyName: "enableCustomization", publicName: "enableCustomization", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { presentationChange: "presentationChange", pointClick: "pointClick", pointAction: "pointAction", selectionChange: "selectionChange", drillDown: "drillDown", crossFilter: "crossFilter", queryRequest: "queryRequest", loadStateChange: "loadStateChange" }, ngImport: i0, template: "<section class=\"analytics-presentation\" [attr.aria-label]=\"projectionId()\">\n <div class=\"analytics-presentation__toolbar\" role=\"group\" [attr.aria-label]=\"t('praxis.charts.analyticsPresentation.selectorLabel', 'Analytics presentation')\">\n @if (eligibleFamilies().includes('chart')) {\n <button type=\"button\" [attr.aria-pressed]=\"activeFamily() === 'chart'\" (click)=\"selectFamily('chart')\">\n {{ t('praxis.charts.analyticsPresentation.chart', 'Chart') }}\n </button>\n }\n @if (eligibleFamilies().includes('analytic-table')) {\n <button type=\"button\" [attr.aria-pressed]=\"activeFamily() === 'analytic-table'\" (click)=\"selectFamily('analytic-table')\">\n {{ t('praxis.charts.analyticsPresentation.table', 'Table') }}\n </button>\n }\n @if (eligibleFamilies().includes('kpi')) {\n <button type=\"button\" [attr.aria-pressed]=\"activeFamily() === 'kpi'\" (click)=\"selectFamily('kpi')\">\n {{ t('praxis.charts.analyticsPresentation.kpi', 'KPI') }}\n </button>\n }\n </div>\n\n @if (diagnostic(); as message) {\n <section class=\"analytics-presentation__state analytics-presentation__state--error\" role=\"alert\">\n <strong>{{ t('praxis.charts.analyticsPresentation.unavailable', 'Presentation unavailable') }}</strong>\n <span>{{ message }}</span>\n </section>\n } @else if (activeFamily() === 'chart' && chartConfig(); as config) {\n <praxis-chart\n [config]=\"config\"\n [queryContext]=\"queryContext()\"\n [enableCustomization]=\"enableCustomization()\"\n (pointClick)=\"pointClick.emit($event)\"\n (pointAction)=\"pointAction.emit($event)\"\n (selectionChange)=\"selectionChange.emit($event)\"\n (drillDown)=\"drillDown.emit($event)\"\n (crossFilter)=\"crossFilter.emit($event)\"\n (queryRequest)=\"queryRequest.emit($event)\"\n (loadStateChange)=\"loadStateChange.emit($event)\"\n />\n } @else if (activeFamily() === 'analytic-table' && tableConfig(); as config) {\n @if (isResultLoading()) {\n <section class=\"analytics-presentation__state\" role=\"status\">{{ t('praxis.charts.analyticsPresentation.loading', 'Loading analytics\u2026') }}</section>\n } @else if (isResultEmpty()) {\n <section class=\"analytics-presentation__state\" role=\"status\">{{ t('praxis.charts.analyticsPresentation.empty', 'No data available.') }}</section>\n } @else {\n <praxis-table\n [config]=\"config\"\n [data]=\"resultRows()\"\n [tableId]=\"projectionId() + '-analytic-table'\"\n (rowClick)=\"handleTableRowClick($event)\"\n />\n }\n } @else if (activeFamily() === 'kpi') {\n @if (isResultLoading()) {\n <section class=\"analytics-presentation__state\" role=\"status\">{{ t('praxis.charts.analyticsPresentation.loading', 'Loading analytics\u2026') }}</section>\n } @else if (isResultEmpty()) {\n <section class=\"analytics-presentation__state\" role=\"status\">{{ t('praxis.charts.analyticsPresentation.empty', 'No data available.') }}</section>\n } @else {\n <section class=\"analytics-presentation__kpi\" [attr.aria-label]=\"kpiTitle() || t('praxis.charts.analyticsPresentation.kpiCollection', 'Key metrics')\">\n @if (kpiTitle(); as heading) {\n <header class=\"analytics-presentation__kpi-header\">\n <h3>{{ heading }}</h3>\n @if (kpiSubtitle(); as description) {\n <p>{{ description }}</p>\n }\n </header>\n }\n <div class=\"analytics-presentation__kpi-grid\" role=\"list\">\n @for (item of kpiItems(); track item.trackId) {\n <div role=\"listitem\">\n <button\n class=\"analytics-presentation__kpi-card\"\n type=\"button\"\n [attr.aria-label]=\"kpiItemAriaLabel(item.label, item.formattedValue, item.metricLabel)\"\n (click)=\"handleKpiClick(item.row, item.label, item.value)\"\n >\n <strong>{{ item.formattedValue }}</strong>\n <span>{{ item.label }}</span>\n <small>{{ item.metricLabel }}</small>\n </button>\n </div>\n }\n </div>\n </section>\n }\n }\n</section>\n", styles: [":host{display:block;min-width:0}.analytics-presentation{display:grid;gap:12px;min-width:0}.analytics-presentation__toolbar{display:flex;flex-wrap:wrap;justify-content:flex-end;gap:4px}.analytics-presentation__toolbar button{min-height:40px;padding:0 14px;border:1px solid var(--md-sys-color-outline-variant, #c4c7c5);border-radius:999px;background:var(--md-sys-color-surface, var(--mat-sys-surface));color:var(--md-sys-color-on-surface, #1a1c1e);font:inherit;cursor:pointer}.analytics-presentation__toolbar button[aria-pressed=true]{border-color:var(--md-sys-color-primary, #1263b4);background:var(--md-sys-color-primary-container, #d7e3ff);color:var(--md-sys-color-on-primary-container, #001b3e)}.analytics-presentation__toolbar button:focus-visible{outline:3px solid color-mix(in srgb,var(--md-sys-color-primary, #1263b4) 45%,transparent);outline-offset:2px}.analytics-presentation__state{min-height:220px;display:grid;place-content:center;gap:6px;padding:24px;border:1px solid var(--md-sys-color-outline-variant, #c4c7c5);border-radius:16px;background:var(--md-sys-color-surface-container-low, #f7f8fa);color:var(--md-sys-color-on-surface, #1a1c1e);text-align:center}.analytics-presentation__state--error{border-color:var(--md-sys-color-error, #ba1a1a);color:var(--md-sys-color-error, #ba1a1a)}.analytics-presentation__kpi{display:grid;gap:16px;min-width:0}.analytics-presentation__kpi-header{display:grid;gap:4px}.analytics-presentation__kpi-header h3,.analytics-presentation__kpi-header p{margin:0}.analytics-presentation__kpi-header h3{color:var(--md-sys-color-on-surface, #1a1c1e);font-size:1rem;font-weight:650}.analytics-presentation__kpi-header p{color:var(--md-sys-color-on-surface-variant, #44474e);font-size:.875rem}.analytics-presentation__kpi-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,176px),1fr));gap:12px;min-width:0}.analytics-presentation__kpi-card{display:grid;gap:6px;width:100%;height:100%;min-width:0;min-height:132px;align-content:center;padding:18px;border:1px solid var(--md-sys-color-outline-variant, #c4c7c5);border-radius:16px;background:var(--md-sys-color-surface-container-low, #f7f8fa);color:var(--md-sys-color-on-surface, #1a1c1e);font:inherit;text-align:left;cursor:pointer}.analytics-presentation__kpi-card:hover{border-color:var(--md-sys-color-primary, #1263b4);background:var(--md-sys-color-surface-container, #f0f1f3)}.analytics-presentation__kpi-card:focus-visible{outline:3px solid color-mix(in srgb,var(--md-sys-color-primary, #1263b4) 45%,transparent);outline-offset:2px}.analytics-presentation__kpi-card strong{overflow-wrap:anywhere;color:var(--md-sys-color-primary, #1263b4);font-size:clamp(1.75rem,5vw,2.5rem);font-variant-numeric:tabular-nums;line-height:1}.analytics-presentation__kpi-card span,.analytics-presentation__kpi-card small{overflow-wrap:anywhere}.analytics-presentation__kpi-card span{font-weight:600}.analytics-presentation__kpi-card small{color:var(--md-sys-color-on-surface-variant, #44474e)}@media(max-width:600px){.analytics-presentation__toolbar{justify-content:stretch}.analytics-presentation__toolbar button{flex:1 1 0}}\n"], dependencies: [{ kind: "component", type: PraxisChartComponent, selector: "praxis-chart", inputs: ["config", "resourceKey", "data", "chartDocument", "filterCriteria", "queryContext", "remoteDataResolver", "enableCustomization", "availableResources", "availableFields", "availableTargets"], outputs: ["pointClick", "pointAction", "selectionChange", "drillDown", "crossFilter", "queryRequest", "loadStateChange", "chartDocumentApplied", "chartDocumentSaved", "chartDocumentPatchChange"] }, { kind: "component", type: PraxisTable, selector: "praxis-table", inputs: ["config", "resourcePath", "data", "tableId", "componentInstanceId", "configPersistenceStrategy", "title", "subtitle", "icon", "autoDelete", "notifyIfOutdated", "snoozeMs", "autoOpenSettingsOnOutdated", "crudContext", "filterCriteria", "filterTeleport", "widgetId", "queryContext", "aiContext", "aiAssistantVoiceInputMode", "aiAssistantVoiceLanguage", "horizontalScroll", "enableCustomization", "authoringCapability", "dense"], outputs: ["filterCriteriaChange", "queryContextClear", "queryContextChange", "rowClick", "widgetEvent", "resourceEvent", "rowDoubleClick", "rowExpansionChange", "rowAction", "toolbarAction", "bulkAction", "exportAction", "columnReorder", "columnReorderAttempt", "columnResize", "beforeDelete", "afterDelete", "deleteError", "beforeBulkDelete", "afterBulkDelete", "bulkDeleteError", "schemaStatusChange", "configChange", "metadataChange", "loadingStateChange", "collectionLinksChange", "selectionChange", "drawerOpen", "drawerClose", "drawerTabChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
7311
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.14", type: PraxisAnalyticsPresentationComponent, isStandalone: true, selector: "praxis-analytics-presentation", inputs: { analytics: { classPropertyName: "analytics", publicName: "analytics", isSignal: true, isRequired: true, transformFunction: null }, projectionId: { classPropertyName: "projectionId", publicName: "projectionId", isSignal: true, isRequired: true, transformFunction: null }, availableFamilies: { classPropertyName: "availableFamilies", publicName: "availableFamilies", isSignal: true, isRequired: false, transformFunction: null }, preferredFamily: { classPropertyName: "preferredFamily", publicName: "preferredFamily", isSignal: true, isRequired: false, transformFunction: null }, queryContext: { classPropertyName: "queryContext", publicName: "queryContext", isSignal: true, isRequired: false, transformFunction: null }, title: { classPropertyName: "title", publicName: "title", isSignal: true, isRequired: false, transformFunction: null }, subtitle: { classPropertyName: "subtitle", publicName: "subtitle", isSignal: true, isRequired: false, transformFunction: null }, height: { classPropertyName: "height", publicName: "height", isSignal: true, isRequired: false, transformFunction: null }, motion: { classPropertyName: "motion", publicName: "motion", isSignal: true, isRequired: false, transformFunction: null }, enableCustomization: { classPropertyName: "enableCustomization", publicName: "enableCustomization", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { presentationChange: "presentationChange", pointClick: "pointClick", pointAction: "pointAction", selectionChange: "selectionChange", drillDown: "drillDown", crossFilter: "crossFilter", queryRequest: "queryRequest", loadStateChange: "loadStateChange" }, ngImport: i0, template: "<section class=\"analytics-presentation\" [attr.aria-label]=\"projectionId()\">\n <div class=\"analytics-presentation__toolbar\" role=\"group\" [attr.aria-label]=\"t('praxis.charts.analyticsPresentation.selectorLabel', 'Analytics presentation')\">\n @if (eligibleFamilies().includes('chart')) {\n <button type=\"button\" [attr.aria-pressed]=\"activeFamily() === 'chart'\" (click)=\"selectFamily('chart')\">\n {{ t('praxis.charts.analyticsPresentation.chart', 'Chart') }}\n </button>\n }\n @if (eligibleFamilies().includes('analytic-table')) {\n <button type=\"button\" [attr.aria-pressed]=\"activeFamily() === 'analytic-table'\" (click)=\"selectFamily('analytic-table')\">\n {{ t('praxis.charts.analyticsPresentation.table', 'Table') }}\n </button>\n }\n @if (eligibleFamilies().includes('kpi')) {\n <button type=\"button\" [attr.aria-pressed]=\"activeFamily() === 'kpi'\" (click)=\"selectFamily('kpi')\">\n {{ t('praxis.charts.analyticsPresentation.kpi', 'KPI') }}\n </button>\n }\n </div>\n\n @if (diagnostic(); as message) {\n <section class=\"analytics-presentation__state analytics-presentation__state--error\" role=\"alert\">\n <strong>{{ t('praxis.charts.analyticsPresentation.unavailable', 'Presentation unavailable') }}</strong>\n <span>{{ message }}</span>\n </section>\n } @else if (activeFamily() === 'chart' && chartConfig(); as config) {\n <praxis-chart\n [config]=\"config\"\n [queryContext]=\"queryContext()\"\n [enableCustomization]=\"enableCustomization()\"\n (pointClick)=\"pointClick.emit($event)\"\n (pointAction)=\"pointAction.emit($event)\"\n (selectionChange)=\"selectionChange.emit($event)\"\n (drillDown)=\"drillDown.emit($event)\"\n (crossFilter)=\"crossFilter.emit($event)\"\n (queryRequest)=\"queryRequest.emit($event)\"\n (loadStateChange)=\"loadStateChange.emit($event)\"\n />\n } @else if (activeFamily() === 'analytic-table' && tableConfig(); as config) {\n @if (isResultLoading()) {\n <section class=\"analytics-presentation__state analytics-presentation__state--loading\" role=\"status\"><span class=\"analytics-presentation__loading-label\">{{ t('praxis.charts.analyticsPresentation.loading', 'Loading analytics\u2026') }}</span></section>\n } @else if (isResultEmpty()) {\n <section class=\"analytics-presentation__state\" role=\"status\">{{ t('praxis.charts.analyticsPresentation.empty', 'No data available.') }}</section>\n } @else {\n <praxis-table\n [config]=\"config\"\n [data]=\"resultRows()\"\n [tableId]=\"projectionId() + '-analytic-table'\"\n (rowClick)=\"handleTableRowClick($event)\"\n />\n }\n } @else if (activeFamily() === 'kpi') {\n @if (isResultLoading()) {\n <section class=\"analytics-presentation__state analytics-presentation__state--loading\" role=\"status\"><span class=\"analytics-presentation__loading-label\">{{ t('praxis.charts.analyticsPresentation.loading', 'Loading analytics\u2026') }}</span></section>\n } @else if (isResultEmpty()) {\n <section class=\"analytics-presentation__state\" role=\"status\">{{ t('praxis.charts.analyticsPresentation.empty', 'No data available.') }}</section>\n } @else {\n <section class=\"analytics-presentation__kpi\" [attr.aria-label]=\"kpiTitle() || t('praxis.charts.analyticsPresentation.kpiCollection', 'Key metrics')\">\n @if (kpiTitle(); as heading) {\n <header class=\"analytics-presentation__kpi-header\">\n <h3>{{ heading }}</h3>\n @if (kpiSubtitle(); as description) {\n <p>{{ description }}</p>\n }\n </header>\n }\n <div class=\"analytics-presentation__kpi-grid\" role=\"list\">\n @for (item of kpiItems(); track item.trackId) {\n <div role=\"listitem\">\n <button\n class=\"analytics-presentation__kpi-card\"\n type=\"button\"\n [attr.aria-label]=\"kpiItemAriaLabel(item.label, item.formattedValue, item.metricLabel)\"\n (click)=\"handleKpiClick(item.row, item.label, item.value)\"\n >\n <strong>{{ item.formattedValue }}</strong>\n <span>{{ item.label }}</span>\n <small>{{ item.metricLabel }}</small>\n </button>\n </div>\n }\n </div>\n </section>\n }\n }\n</section>\n", styles: [":host{display:block;min-width:0}.analytics-presentation{display:grid;gap:12px;min-width:0}.analytics-presentation__toolbar{display:flex;flex-wrap:wrap;justify-content:flex-end;gap:4px}.analytics-presentation__toolbar button{min-height:40px;padding:0 14px;border:1px solid var(--md-sys-color-outline-variant, #c4c7c5);border-radius:999px;background:var(--md-sys-color-surface, var(--mat-sys-surface));color:var(--md-sys-color-on-surface, #1a1c1e);font:inherit;cursor:pointer}.analytics-presentation__toolbar button[aria-pressed=true]{border-color:var(--md-sys-color-primary, #1263b4);background:var(--md-sys-color-primary-container, #d7e3ff);color:var(--md-sys-color-on-primary-container, #001b3e)}.analytics-presentation__toolbar button:focus-visible{outline:3px solid color-mix(in srgb,var(--md-sys-color-primary, #1263b4) 45%,transparent);outline-offset:2px}.analytics-presentation__state{min-height:220px;display:grid;place-content:center;gap:6px;padding:24px;border:1px solid var(--md-sys-color-outline-variant, #c4c7c5);border-radius:16px;background:var(--md-sys-color-surface-container-low, #f7f8fa);color:var(--md-sys-color-on-surface, #1a1c1e);text-align:center}.analytics-presentation__state--error{border-color:var(--md-sys-color-error, #ba1a1a);color:var(--md-sys-color-error, #ba1a1a)}.analytics-presentation__loading-label{font-weight:600;background-image:var(--pdx-text-shimmer-gradient, linear-gradient(105deg, var(--md-sys-color-on-surface-variant, #44474e) 0%, var(--md-sys-color-on-surface-variant, #44474e) 28%, var(--md-sys-color-primary, #1263b4) 44%, var(--md-sys-color-tertiary, #7c3aed) 56%, var(--md-sys-color-on-surface-variant, #44474e) 72%, var(--md-sys-color-on-surface-variant, #44474e) 100%));background-size:260% 100%;background-position:200% 50%;-webkit-background-clip:text;background-clip:text;color:transparent;-webkit-text-fill-color:transparent;animation:praxis-analytics-loading-shimmer var(--pdx-text-shimmer-duration, 2.4s) linear infinite}@media(prefers-reduced-motion:reduce),(forced-colors:active),print{.analytics-presentation__loading-label{animation:none;background-image:none;color:var(--pdx-text-shimmer-fallback-color, var(--md-sys-color-on-surface, #1a1c1e));-webkit-text-fill-color:currentColor}}@media(forced-colors:active){.analytics-presentation__loading-label{color:CanvasText!important;-webkit-text-fill-color:CanvasText!important}}@keyframes praxis-analytics-loading-shimmer{0%{background-position:200% 50%}to{background-position:-60% 50%}}.analytics-presentation__kpi{display:grid;gap:16px;min-width:0}.analytics-presentation__kpi-header{display:grid;gap:4px}.analytics-presentation__kpi-header h3,.analytics-presentation__kpi-header p{margin:0}.analytics-presentation__kpi-header h3{color:var(--md-sys-color-on-surface, #1a1c1e);font-size:1rem;font-weight:650}.analytics-presentation__kpi-header p{color:var(--md-sys-color-on-surface-variant, #44474e);font-size:.875rem}.analytics-presentation__kpi-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,176px),1fr));gap:12px;min-width:0}.analytics-presentation__kpi-card{display:grid;gap:6px;width:100%;height:100%;min-width:0;min-height:132px;align-content:center;padding:18px;border:1px solid var(--md-sys-color-outline-variant, #c4c7c5);border-radius:16px;background:var(--md-sys-color-surface-container-low, #f7f8fa);color:var(--md-sys-color-on-surface, #1a1c1e);font:inherit;text-align:left;cursor:pointer}.analytics-presentation__kpi-card:hover{border-color:var(--md-sys-color-primary, #1263b4);background:var(--md-sys-color-surface-container, #f0f1f3)}.analytics-presentation__kpi-card:focus-visible{outline:3px solid color-mix(in srgb,var(--md-sys-color-primary, #1263b4) 45%,transparent);outline-offset:2px}.analytics-presentation__kpi-card strong{overflow-wrap:anywhere;color:var(--md-sys-color-primary, #1263b4);font-size:clamp(1.75rem,5vw,2.5rem);font-variant-numeric:tabular-nums;line-height:1}.analytics-presentation__kpi-card span,.analytics-presentation__kpi-card small{overflow-wrap:anywhere}.analytics-presentation__kpi-card span{font-weight:600}.analytics-presentation__kpi-card small{color:var(--md-sys-color-on-surface-variant, #44474e)}@media(max-width:600px){.analytics-presentation__toolbar{justify-content:stretch}.analytics-presentation__toolbar button{flex:1 1 0}}\n"], dependencies: [{ kind: "component", type: PraxisChartComponent, selector: "praxis-chart", inputs: ["config", "resourceKey", "data", "chartDocument", "filterCriteria", "queryContext", "remoteDataResolver", "enableCustomization", "availableResources", "availableFields", "availableTargets"], outputs: ["pointClick", "pointAction", "selectionChange", "drillDown", "crossFilter", "queryRequest", "loadStateChange", "chartDocumentApplied", "chartDocumentSaved", "chartDocumentPatchChange"] }, { kind: "component", type: PraxisTable, selector: "praxis-table", inputs: ["config", "resourcePath", "data", "tableId", "componentInstanceId", "configPersistenceStrategy", "title", "subtitle", "icon", "autoDelete", "notifyIfOutdated", "snoozeMs", "autoOpenSettingsOnOutdated", "crudContext", "filterCriteria", "filterTeleport", "widgetId", "queryContext", "aiContext", "aiAssistantVoiceInputMode", "aiAssistantVoiceLanguage", "horizontalScroll", "enableCustomization", "authoringCapability", "dense"], outputs: ["filterCriteriaChange", "queryContextClear", "queryContextChange", "rowClick", "widgetEvent", "resourceEvent", "rowDoubleClick", "rowExpansionChange", "rowAction", "toolbarAction", "bulkAction", "exportAction", "columnReorder", "columnReorderAttempt", "columnResize", "beforeDelete", "afterDelete", "deleteError", "beforeBulkDelete", "afterBulkDelete", "bulkDeleteError", "schemaStatusChange", "configChange", "metadataChange", "loadingStateChange", "collectionLinksChange", "selectionChange", "drawerOpen", "drawerClose", "drawerTabChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
6344
7312
|
}
|
|
6345
7313
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: PraxisAnalyticsPresentationComponent, decorators: [{
|
|
6346
7314
|
type: Component,
|
|
6347
|
-
args: [{ selector: 'praxis-analytics-presentation', imports: [PraxisChartComponent, PraxisTable], changeDetection: ChangeDetectionStrategy.OnPush, template: "<section class=\"analytics-presentation\" [attr.aria-label]=\"projectionId()\">\n <div class=\"analytics-presentation__toolbar\" role=\"group\" [attr.aria-label]=\"t('praxis.charts.analyticsPresentation.selectorLabel', 'Analytics presentation')\">\n @if (eligibleFamilies().includes('chart')) {\n <button type=\"button\" [attr.aria-pressed]=\"activeFamily() === 'chart'\" (click)=\"selectFamily('chart')\">\n {{ t('praxis.charts.analyticsPresentation.chart', 'Chart') }}\n </button>\n }\n @if (eligibleFamilies().includes('analytic-table')) {\n <button type=\"button\" [attr.aria-pressed]=\"activeFamily() === 'analytic-table'\" (click)=\"selectFamily('analytic-table')\">\n {{ t('praxis.charts.analyticsPresentation.table', 'Table') }}\n </button>\n }\n @if (eligibleFamilies().includes('kpi')) {\n <button type=\"button\" [attr.aria-pressed]=\"activeFamily() === 'kpi'\" (click)=\"selectFamily('kpi')\">\n {{ t('praxis.charts.analyticsPresentation.kpi', 'KPI') }}\n </button>\n }\n </div>\n\n @if (diagnostic(); as message) {\n <section class=\"analytics-presentation__state analytics-presentation__state--error\" role=\"alert\">\n <strong>{{ t('praxis.charts.analyticsPresentation.unavailable', 'Presentation unavailable') }}</strong>\n <span>{{ message }}</span>\n </section>\n } @else if (activeFamily() === 'chart' && chartConfig(); as config) {\n <praxis-chart\n [config]=\"config\"\n [queryContext]=\"queryContext()\"\n [enableCustomization]=\"enableCustomization()\"\n (pointClick)=\"pointClick.emit($event)\"\n (pointAction)=\"pointAction.emit($event)\"\n (selectionChange)=\"selectionChange.emit($event)\"\n (drillDown)=\"drillDown.emit($event)\"\n (crossFilter)=\"crossFilter.emit($event)\"\n (queryRequest)=\"queryRequest.emit($event)\"\n (loadStateChange)=\"loadStateChange.emit($event)\"\n />\n } @else if (activeFamily() === 'analytic-table' && tableConfig(); as config) {\n @if (isResultLoading()) {\n <section class=\"analytics-presentation__state\" role=\"status\">{{ t('praxis.charts.analyticsPresentation.loading', 'Loading analytics\u2026') }}</section>\n } @else if (isResultEmpty()) {\n <section class=\"analytics-presentation__state\" role=\"status\">{{ t('praxis.charts.analyticsPresentation.empty', 'No data available.') }}</section>\n } @else {\n <praxis-table\n [config]=\"config\"\n [data]=\"resultRows()\"\n [tableId]=\"projectionId() + '-analytic-table'\"\n (rowClick)=\"handleTableRowClick($event)\"\n />\n }\n } @else if (activeFamily() === 'kpi') {\n @if (isResultLoading()) {\n <section class=\"analytics-presentation__state\" role=\"status\">{{ t('praxis.charts.analyticsPresentation.loading', 'Loading analytics\u2026') }}</section>\n } @else if (isResultEmpty()) {\n <section class=\"analytics-presentation__state\" role=\"status\">{{ t('praxis.charts.analyticsPresentation.empty', 'No data available.') }}</section>\n } @else {\n <section class=\"analytics-presentation__kpi\" [attr.aria-label]=\"kpiTitle() || t('praxis.charts.analyticsPresentation.kpiCollection', 'Key metrics')\">\n @if (kpiTitle(); as heading) {\n <header class=\"analytics-presentation__kpi-header\">\n <h3>{{ heading }}</h3>\n @if (kpiSubtitle(); as description) {\n <p>{{ description }}</p>\n }\n </header>\n }\n <div class=\"analytics-presentation__kpi-grid\" role=\"list\">\n @for (item of kpiItems(); track item.trackId) {\n <div role=\"listitem\">\n <button\n class=\"analytics-presentation__kpi-card\"\n type=\"button\"\n [attr.aria-label]=\"kpiItemAriaLabel(item.label, item.formattedValue, item.metricLabel)\"\n (click)=\"handleKpiClick(item.row, item.label, item.value)\"\n >\n <strong>{{ item.formattedValue }}</strong>\n <span>{{ item.label }}</span>\n <small>{{ item.metricLabel }}</small>\n </button>\n </div>\n }\n </div>\n </section>\n }\n }\n</section>\n", styles: [":host{display:block;min-width:0}.analytics-presentation{display:grid;gap:12px;min-width:0}.analytics-presentation__toolbar{display:flex;flex-wrap:wrap;justify-content:flex-end;gap:4px}.analytics-presentation__toolbar button{min-height:40px;padding:0 14px;border:1px solid var(--md-sys-color-outline-variant, #c4c7c5);border-radius:999px;background:var(--md-sys-color-surface, var(--mat-sys-surface));color:var(--md-sys-color-on-surface, #1a1c1e);font:inherit;cursor:pointer}.analytics-presentation__toolbar button[aria-pressed=true]{border-color:var(--md-sys-color-primary, #1263b4);background:var(--md-sys-color-primary-container, #d7e3ff);color:var(--md-sys-color-on-primary-container, #001b3e)}.analytics-presentation__toolbar button:focus-visible{outline:3px solid color-mix(in srgb,var(--md-sys-color-primary, #1263b4) 45%,transparent);outline-offset:2px}.analytics-presentation__state{min-height:220px;display:grid;place-content:center;gap:6px;padding:24px;border:1px solid var(--md-sys-color-outline-variant, #c4c7c5);border-radius:16px;background:var(--md-sys-color-surface-container-low, #f7f8fa);color:var(--md-sys-color-on-surface, #1a1c1e);text-align:center}.analytics-presentation__state--error{border-color:var(--md-sys-color-error, #ba1a1a);color:var(--md-sys-color-error, #ba1a1a)}.analytics-presentation__kpi{display:grid;gap:16px;min-width:0}.analytics-presentation__kpi-header{display:grid;gap:4px}.analytics-presentation__kpi-header h3,.analytics-presentation__kpi-header p{margin:0}.analytics-presentation__kpi-header h3{color:var(--md-sys-color-on-surface, #1a1c1e);font-size:1rem;font-weight:650}.analytics-presentation__kpi-header p{color:var(--md-sys-color-on-surface-variant, #44474e);font-size:.875rem}.analytics-presentation__kpi-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,176px),1fr));gap:12px;min-width:0}.analytics-presentation__kpi-card{display:grid;gap:6px;width:100%;height:100%;min-width:0;min-height:132px;align-content:center;padding:18px;border:1px solid var(--md-sys-color-outline-variant, #c4c7c5);border-radius:16px;background:var(--md-sys-color-surface-container-low, #f7f8fa);color:var(--md-sys-color-on-surface, #1a1c1e);font:inherit;text-align:left;cursor:pointer}.analytics-presentation__kpi-card:hover{border-color:var(--md-sys-color-primary, #1263b4);background:var(--md-sys-color-surface-container, #f0f1f3)}.analytics-presentation__kpi-card:focus-visible{outline:3px solid color-mix(in srgb,var(--md-sys-color-primary, #1263b4) 45%,transparent);outline-offset:2px}.analytics-presentation__kpi-card strong{overflow-wrap:anywhere;color:var(--md-sys-color-primary, #1263b4);font-size:clamp(1.75rem,5vw,2.5rem);font-variant-numeric:tabular-nums;line-height:1}.analytics-presentation__kpi-card span,.analytics-presentation__kpi-card small{overflow-wrap:anywhere}.analytics-presentation__kpi-card span{font-weight:600}.analytics-presentation__kpi-card small{color:var(--md-sys-color-on-surface-variant, #44474e)}@media(max-width:600px){.analytics-presentation__toolbar{justify-content:stretch}.analytics-presentation__toolbar button{flex:1 1 0}}\n"] }]
|
|
7315
|
+
args: [{ selector: 'praxis-analytics-presentation', imports: [PraxisChartComponent, PraxisTable], changeDetection: ChangeDetectionStrategy.OnPush, template: "<section class=\"analytics-presentation\" [attr.aria-label]=\"projectionId()\">\n <div class=\"analytics-presentation__toolbar\" role=\"group\" [attr.aria-label]=\"t('praxis.charts.analyticsPresentation.selectorLabel', 'Analytics presentation')\">\n @if (eligibleFamilies().includes('chart')) {\n <button type=\"button\" [attr.aria-pressed]=\"activeFamily() === 'chart'\" (click)=\"selectFamily('chart')\">\n {{ t('praxis.charts.analyticsPresentation.chart', 'Chart') }}\n </button>\n }\n @if (eligibleFamilies().includes('analytic-table')) {\n <button type=\"button\" [attr.aria-pressed]=\"activeFamily() === 'analytic-table'\" (click)=\"selectFamily('analytic-table')\">\n {{ t('praxis.charts.analyticsPresentation.table', 'Table') }}\n </button>\n }\n @if (eligibleFamilies().includes('kpi')) {\n <button type=\"button\" [attr.aria-pressed]=\"activeFamily() === 'kpi'\" (click)=\"selectFamily('kpi')\">\n {{ t('praxis.charts.analyticsPresentation.kpi', 'KPI') }}\n </button>\n }\n </div>\n\n @if (diagnostic(); as message) {\n <section class=\"analytics-presentation__state analytics-presentation__state--error\" role=\"alert\">\n <strong>{{ t('praxis.charts.analyticsPresentation.unavailable', 'Presentation unavailable') }}</strong>\n <span>{{ message }}</span>\n </section>\n } @else if (activeFamily() === 'chart' && chartConfig(); as config) {\n <praxis-chart\n [config]=\"config\"\n [queryContext]=\"queryContext()\"\n [enableCustomization]=\"enableCustomization()\"\n (pointClick)=\"pointClick.emit($event)\"\n (pointAction)=\"pointAction.emit($event)\"\n (selectionChange)=\"selectionChange.emit($event)\"\n (drillDown)=\"drillDown.emit($event)\"\n (crossFilter)=\"crossFilter.emit($event)\"\n (queryRequest)=\"queryRequest.emit($event)\"\n (loadStateChange)=\"loadStateChange.emit($event)\"\n />\n } @else if (activeFamily() === 'analytic-table' && tableConfig(); as config) {\n @if (isResultLoading()) {\n <section class=\"analytics-presentation__state analytics-presentation__state--loading\" role=\"status\"><span class=\"analytics-presentation__loading-label\">{{ t('praxis.charts.analyticsPresentation.loading', 'Loading analytics\u2026') }}</span></section>\n } @else if (isResultEmpty()) {\n <section class=\"analytics-presentation__state\" role=\"status\">{{ t('praxis.charts.analyticsPresentation.empty', 'No data available.') }}</section>\n } @else {\n <praxis-table\n [config]=\"config\"\n [data]=\"resultRows()\"\n [tableId]=\"projectionId() + '-analytic-table'\"\n (rowClick)=\"handleTableRowClick($event)\"\n />\n }\n } @else if (activeFamily() === 'kpi') {\n @if (isResultLoading()) {\n <section class=\"analytics-presentation__state analytics-presentation__state--loading\" role=\"status\"><span class=\"analytics-presentation__loading-label\">{{ t('praxis.charts.analyticsPresentation.loading', 'Loading analytics\u2026') }}</span></section>\n } @else if (isResultEmpty()) {\n <section class=\"analytics-presentation__state\" role=\"status\">{{ t('praxis.charts.analyticsPresentation.empty', 'No data available.') }}</section>\n } @else {\n <section class=\"analytics-presentation__kpi\" [attr.aria-label]=\"kpiTitle() || t('praxis.charts.analyticsPresentation.kpiCollection', 'Key metrics')\">\n @if (kpiTitle(); as heading) {\n <header class=\"analytics-presentation__kpi-header\">\n <h3>{{ heading }}</h3>\n @if (kpiSubtitle(); as description) {\n <p>{{ description }}</p>\n }\n </header>\n }\n <div class=\"analytics-presentation__kpi-grid\" role=\"list\">\n @for (item of kpiItems(); track item.trackId) {\n <div role=\"listitem\">\n <button\n class=\"analytics-presentation__kpi-card\"\n type=\"button\"\n [attr.aria-label]=\"kpiItemAriaLabel(item.label, item.formattedValue, item.metricLabel)\"\n (click)=\"handleKpiClick(item.row, item.label, item.value)\"\n >\n <strong>{{ item.formattedValue }}</strong>\n <span>{{ item.label }}</span>\n <small>{{ item.metricLabel }}</small>\n </button>\n </div>\n }\n </div>\n </section>\n }\n }\n</section>\n", styles: [":host{display:block;min-width:0}.analytics-presentation{display:grid;gap:12px;min-width:0}.analytics-presentation__toolbar{display:flex;flex-wrap:wrap;justify-content:flex-end;gap:4px}.analytics-presentation__toolbar button{min-height:40px;padding:0 14px;border:1px solid var(--md-sys-color-outline-variant, #c4c7c5);border-radius:999px;background:var(--md-sys-color-surface, var(--mat-sys-surface));color:var(--md-sys-color-on-surface, #1a1c1e);font:inherit;cursor:pointer}.analytics-presentation__toolbar button[aria-pressed=true]{border-color:var(--md-sys-color-primary, #1263b4);background:var(--md-sys-color-primary-container, #d7e3ff);color:var(--md-sys-color-on-primary-container, #001b3e)}.analytics-presentation__toolbar button:focus-visible{outline:3px solid color-mix(in srgb,var(--md-sys-color-primary, #1263b4) 45%,transparent);outline-offset:2px}.analytics-presentation__state{min-height:220px;display:grid;place-content:center;gap:6px;padding:24px;border:1px solid var(--md-sys-color-outline-variant, #c4c7c5);border-radius:16px;background:var(--md-sys-color-surface-container-low, #f7f8fa);color:var(--md-sys-color-on-surface, #1a1c1e);text-align:center}.analytics-presentation__state--error{border-color:var(--md-sys-color-error, #ba1a1a);color:var(--md-sys-color-error, #ba1a1a)}.analytics-presentation__loading-label{font-weight:600;background-image:var(--pdx-text-shimmer-gradient, linear-gradient(105deg, var(--md-sys-color-on-surface-variant, #44474e) 0%, var(--md-sys-color-on-surface-variant, #44474e) 28%, var(--md-sys-color-primary, #1263b4) 44%, var(--md-sys-color-tertiary, #7c3aed) 56%, var(--md-sys-color-on-surface-variant, #44474e) 72%, var(--md-sys-color-on-surface-variant, #44474e) 100%));background-size:260% 100%;background-position:200% 50%;-webkit-background-clip:text;background-clip:text;color:transparent;-webkit-text-fill-color:transparent;animation:praxis-analytics-loading-shimmer var(--pdx-text-shimmer-duration, 2.4s) linear infinite}@media(prefers-reduced-motion:reduce),(forced-colors:active),print{.analytics-presentation__loading-label{animation:none;background-image:none;color:var(--pdx-text-shimmer-fallback-color, var(--md-sys-color-on-surface, #1a1c1e));-webkit-text-fill-color:currentColor}}@media(forced-colors:active){.analytics-presentation__loading-label{color:CanvasText!important;-webkit-text-fill-color:CanvasText!important}}@keyframes praxis-analytics-loading-shimmer{0%{background-position:200% 50%}to{background-position:-60% 50%}}.analytics-presentation__kpi{display:grid;gap:16px;min-width:0}.analytics-presentation__kpi-header{display:grid;gap:4px}.analytics-presentation__kpi-header h3,.analytics-presentation__kpi-header p{margin:0}.analytics-presentation__kpi-header h3{color:var(--md-sys-color-on-surface, #1a1c1e);font-size:1rem;font-weight:650}.analytics-presentation__kpi-header p{color:var(--md-sys-color-on-surface-variant, #44474e);font-size:.875rem}.analytics-presentation__kpi-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,176px),1fr));gap:12px;min-width:0}.analytics-presentation__kpi-card{display:grid;gap:6px;width:100%;height:100%;min-width:0;min-height:132px;align-content:center;padding:18px;border:1px solid var(--md-sys-color-outline-variant, #c4c7c5);border-radius:16px;background:var(--md-sys-color-surface-container-low, #f7f8fa);color:var(--md-sys-color-on-surface, #1a1c1e);font:inherit;text-align:left;cursor:pointer}.analytics-presentation__kpi-card:hover{border-color:var(--md-sys-color-primary, #1263b4);background:var(--md-sys-color-surface-container, #f0f1f3)}.analytics-presentation__kpi-card:focus-visible{outline:3px solid color-mix(in srgb,var(--md-sys-color-primary, #1263b4) 45%,transparent);outline-offset:2px}.analytics-presentation__kpi-card strong{overflow-wrap:anywhere;color:var(--md-sys-color-primary, #1263b4);font-size:clamp(1.75rem,5vw,2.5rem);font-variant-numeric:tabular-nums;line-height:1}.analytics-presentation__kpi-card span,.analytics-presentation__kpi-card small{overflow-wrap:anywhere}.analytics-presentation__kpi-card span{font-weight:600}.analytics-presentation__kpi-card small{color:var(--md-sys-color-on-surface-variant, #44474e)}@media(max-width:600px){.analytics-presentation__toolbar{justify-content:stretch}.analytics-presentation__toolbar button{flex:1 1 0}}\n"] }]
|
|
6348
7316
|
}], ctorParameters: () => [], propDecorators: { analytics: [{ type: i0.Input, args: [{ isSignal: true, alias: "analytics", required: true }] }], projectionId: [{ type: i0.Input, args: [{ isSignal: true, alias: "projectionId", required: true }] }], availableFamilies: [{ type: i0.Input, args: [{ isSignal: true, alias: "availableFamilies", required: false }] }], preferredFamily: [{ type: i0.Input, args: [{ isSignal: true, alias: "preferredFamily", required: false }] }], queryContext: [{ type: i0.Input, args: [{ isSignal: true, alias: "queryContext", required: false }] }], title: [{ type: i0.Input, args: [{ isSignal: true, alias: "title", required: false }] }], subtitle: [{ type: i0.Input, args: [{ isSignal: true, alias: "subtitle", required: false }] }], height: [{ type: i0.Input, args: [{ isSignal: true, alias: "height", required: false }] }], motion: [{ type: i0.Input, args: [{ isSignal: true, alias: "motion", required: false }] }], enableCustomization: [{ type: i0.Input, args: [{ isSignal: true, alias: "enableCustomization", required: false }] }], presentationChange: [{ type: i0.Output, args: ["presentationChange"] }], pointClick: [{ type: i0.Output, args: ["pointClick"] }], pointAction: [{ type: i0.Output, args: ["pointAction"] }], selectionChange: [{ type: i0.Output, args: ["selectionChange"] }], drillDown: [{ type: i0.Output, args: ["drillDown"] }], crossFilter: [{ type: i0.Output, args: ["crossFilter"] }], queryRequest: [{ type: i0.Output, args: ["queryRequest"] }], loadStateChange: [{ type: i0.Output, args: ["loadStateChange"] }] } });
|
|
6349
7317
|
function stringifyValue(value) {
|
|
6350
7318
|
return value === null || value === undefined ? undefined : String(value);
|
|
@@ -6609,6 +7577,7 @@ class EChartsOptionBuilderService {
|
|
|
6609
7577
|
name: gauge.name,
|
|
6610
7578
|
value: gauge.value,
|
|
6611
7579
|
itemStyle: gaugeColor ? { color: gaugeColor } : undefined,
|
|
7580
|
+
...(this.pointTooltipSuppression(config, config.series[0]?.id, config.series[0]?.name, gauge.data) ?? {}),
|
|
6612
7581
|
}],
|
|
6613
7582
|
};
|
|
6614
7583
|
return {
|
|
@@ -6664,6 +7633,7 @@ class EChartsOptionBuilderService {
|
|
|
6664
7633
|
name: slice.name,
|
|
6665
7634
|
value: slice.value,
|
|
6666
7635
|
itemStyle: slice.color ? { color: slice.color } : undefined,
|
|
7636
|
+
...(this.pointTooltipSuppression(config, config.series[0]?.id, config.series[0]?.name, slice.data) ?? {}),
|
|
6667
7637
|
})),
|
|
6668
7638
|
};
|
|
6669
7639
|
return {
|
|
@@ -6718,6 +7688,7 @@ class EChartsOptionBuilderService {
|
|
|
6718
7688
|
name: slice.name,
|
|
6719
7689
|
value: slice.value,
|
|
6720
7690
|
itemStyle: slice.color ? { color: slice.color } : undefined,
|
|
7691
|
+
...(this.pointTooltipSuppression(config, config.series[0]?.id, config.series[0]?.name, slice.data) ?? {}),
|
|
6721
7692
|
})),
|
|
6722
7693
|
};
|
|
6723
7694
|
const baseOption = {
|
|
@@ -6788,6 +7759,7 @@ class EChartsOptionBuilderService {
|
|
|
6788
7759
|
name: slice.name,
|
|
6789
7760
|
value: slice.value,
|
|
6790
7761
|
itemStyle: slice.color ? { color: slice.color } : undefined,
|
|
7762
|
+
...(this.pointTooltipSuppression(config, config.series[0]?.id, config.series[0]?.name, slice.data) ?? {}),
|
|
6791
7763
|
})),
|
|
6792
7764
|
};
|
|
6793
7765
|
return {
|
|
@@ -6806,6 +7778,122 @@ class EChartsOptionBuilderService {
|
|
|
6806
7778
|
series: [treemapSeries],
|
|
6807
7779
|
};
|
|
6808
7780
|
}
|
|
7781
|
+
if (transformed.mode === 'heatmap' && transformed.heatmap) {
|
|
7782
|
+
const heatmap = transformed.heatmap;
|
|
7783
|
+
const format = this.primaryValueFormat(config);
|
|
7784
|
+
const labelsVisible = heatmap.labelsVisible ?? false;
|
|
7785
|
+
const basePrimaryColor = heatmap.color ?? palette[0] ?? '#1263b4';
|
|
7786
|
+
const baseSecondaryColor = palette[1] ?? '#0f766e';
|
|
7787
|
+
const baseAccentColor = palette[2] ?? '#f08c00';
|
|
7788
|
+
const isDarkSurface = this.isLightTextColor(textColor);
|
|
7789
|
+
const lowStopColor = isDarkSurface ? '#1e293b' : '#e2e8f0';
|
|
7790
|
+
const defaultCellBorderColor = isDarkSurface
|
|
7791
|
+
? 'rgba(15, 23, 42, 0.55)'
|
|
7792
|
+
: 'rgba(255, 255, 255, 0.65)';
|
|
7793
|
+
const heatmapSeries = {
|
|
7794
|
+
id: heatmap.id,
|
|
7795
|
+
name: heatmap.name,
|
|
7796
|
+
type: 'heatmap',
|
|
7797
|
+
universalTransition,
|
|
7798
|
+
label: {
|
|
7799
|
+
show: labelsVisible,
|
|
7800
|
+
color: textColor,
|
|
7801
|
+
formatter: labelsVisible
|
|
7802
|
+
? (params) => this.formatValue(params.value, format)
|
|
7803
|
+
: undefined,
|
|
7804
|
+
},
|
|
7805
|
+
emphasis: {
|
|
7806
|
+
itemStyle: {
|
|
7807
|
+
shadowBlur: 8,
|
|
7808
|
+
shadowColor: 'rgba(15, 23, 42, 0.28)',
|
|
7809
|
+
},
|
|
7810
|
+
},
|
|
7811
|
+
itemStyle: {
|
|
7812
|
+
borderWidth: 1,
|
|
7813
|
+
borderColor: config.theme?.backgroundColor ?? defaultCellBorderColor,
|
|
7814
|
+
borderRadius: 2,
|
|
7815
|
+
},
|
|
7816
|
+
data: heatmap.cells.map((cell) => ({
|
|
7817
|
+
...cell.data,
|
|
7818
|
+
name: `${cell.xCategory} • ${cell.yCategory}`,
|
|
7819
|
+
value: cell.value,
|
|
7820
|
+
...(this.pointTooltipSuppression(config, heatmap.id, heatmap.name, cell.data) ?? {}),
|
|
7821
|
+
})),
|
|
7822
|
+
};
|
|
7823
|
+
const hasChartTitle = this.hasText(config.title) || this.hasText(config.subtitle);
|
|
7824
|
+
return {
|
|
7825
|
+
...motion,
|
|
7826
|
+
aria: accessibility,
|
|
7827
|
+
backgroundColor: config.theme?.backgroundColor,
|
|
7828
|
+
color: palette,
|
|
7829
|
+
title: this.buildTitle(config, 'left', textColor),
|
|
7830
|
+
tooltip: tooltipEnabled
|
|
7831
|
+
? {
|
|
7832
|
+
trigger: 'item',
|
|
7833
|
+
confine: true,
|
|
7834
|
+
appendToBody: false,
|
|
7835
|
+
formatter: (params) => this.formatHeatmapTooltip(params, heatmap, format),
|
|
7836
|
+
}
|
|
7837
|
+
: undefined,
|
|
7838
|
+
legend: { show: false },
|
|
7839
|
+
grid: {
|
|
7840
|
+
top: hasChartTitle ? CARTESIAN_GRID_TOP_WITH_TITLE : CARTESIAN_GRID_TOP_WITHOUT_TITLE,
|
|
7841
|
+
right: 28,
|
|
7842
|
+
bottom: legendVisible ? 68 : 40,
|
|
7843
|
+
left: 56,
|
|
7844
|
+
outerBoundsMode: 'same',
|
|
7845
|
+
outerBoundsContain: 'axisLabel',
|
|
7846
|
+
},
|
|
7847
|
+
xAxis: {
|
|
7848
|
+
type: 'category',
|
|
7849
|
+
name: config.axes?.x?.label,
|
|
7850
|
+
nameTextStyle: { color: textColor },
|
|
7851
|
+
data: heatmap.xCategories,
|
|
7852
|
+
nameLocation: 'middle',
|
|
7853
|
+
nameGap: 24,
|
|
7854
|
+
splitArea: { show: true },
|
|
7855
|
+
axisLabel: {
|
|
7856
|
+
show: config.axes?.x?.labels?.visible ?? true,
|
|
7857
|
+
color: textColor,
|
|
7858
|
+
rotate: config.axes?.x?.labels?.rotate ?? 0,
|
|
7859
|
+
hideOverlap: true,
|
|
7860
|
+
formatter: this.axisLabelFormatter(config.axes?.x?.labels?.format),
|
|
7861
|
+
},
|
|
7862
|
+
},
|
|
7863
|
+
yAxis: {
|
|
7864
|
+
type: 'category',
|
|
7865
|
+
name: config.axes?.y?.label,
|
|
7866
|
+
nameTextStyle: { color: textColor },
|
|
7867
|
+
data: heatmap.yCategories,
|
|
7868
|
+
nameLocation: 'middle',
|
|
7869
|
+
nameGap: 44,
|
|
7870
|
+
splitArea: { show: true },
|
|
7871
|
+
axisLabel: {
|
|
7872
|
+
show: config.axes?.y?.labels?.visible ?? true,
|
|
7873
|
+
color: textColor,
|
|
7874
|
+
hideOverlap: true,
|
|
7875
|
+
formatter: this.axisLabelFormatter(config.axes?.y?.labels?.format),
|
|
7876
|
+
},
|
|
7877
|
+
},
|
|
7878
|
+
visualMap: {
|
|
7879
|
+
type: 'continuous',
|
|
7880
|
+
show: legendVisible,
|
|
7881
|
+
min: heatmap.min,
|
|
7882
|
+
max: heatmap.max,
|
|
7883
|
+
calculable: true,
|
|
7884
|
+
orient: 'horizontal',
|
|
7885
|
+
left: 'center',
|
|
7886
|
+
bottom: 10,
|
|
7887
|
+
itemWidth: 12,
|
|
7888
|
+
itemHeight: 140,
|
|
7889
|
+
textStyle: { color: textColor, fontSize: 11 },
|
|
7890
|
+
inRange: {
|
|
7891
|
+
color: [lowStopColor, baseSecondaryColor, basePrimaryColor, baseAccentColor],
|
|
7892
|
+
},
|
|
7893
|
+
},
|
|
7894
|
+
series: [heatmapSeries],
|
|
7895
|
+
};
|
|
7896
|
+
}
|
|
6809
7897
|
if (transformed.mode === 'scatter') {
|
|
6810
7898
|
const scatterSeries = transformed.series.map((series) => ({
|
|
6811
7899
|
id: series.id,
|
|
@@ -6820,7 +7908,7 @@ class EChartsOptionBuilderService {
|
|
|
6820
7908
|
: undefined,
|
|
6821
7909
|
},
|
|
6822
7910
|
itemStyle: series.color ? { color: series.color } : undefined,
|
|
6823
|
-
data: series.points,
|
|
7911
|
+
data: series.points.map((point) => this.decorateSeriesPointWithQuickViewTooltipSuppression(config, series.id, series.name, point)),
|
|
6824
7912
|
}));
|
|
6825
7913
|
return {
|
|
6826
7914
|
...motion,
|
|
@@ -6956,7 +8044,7 @@ class EChartsOptionBuilderService {
|
|
|
6956
8044
|
: undefined,
|
|
6957
8045
|
},
|
|
6958
8046
|
itemStyle: series.color ? { color: series.color } : undefined,
|
|
6959
|
-
data: series.points,
|
|
8047
|
+
data: series.points.map((point) => this.decorateSeriesPointWithQuickViewTooltipSuppression(config, series.id, series.name, point)),
|
|
6960
8048
|
})),
|
|
6961
8049
|
};
|
|
6962
8050
|
}
|
|
@@ -7020,14 +8108,18 @@ class EChartsOptionBuilderService {
|
|
|
7020
8108
|
const title = this.i18n.resolve(config.title).trim();
|
|
7021
8109
|
const seriesCount = transformed.mode === 'gauge'
|
|
7022
8110
|
? transformed.gauge ? 1 : 0
|
|
7023
|
-
: transformed.
|
|
7024
|
-
? 1
|
|
7025
|
-
: transformed.
|
|
8111
|
+
: transformed.mode === 'heatmap'
|
|
8112
|
+
? transformed.heatmap ? 1 : 0
|
|
8113
|
+
: transformed.slices.length
|
|
8114
|
+
? 1
|
|
8115
|
+
: transformed.series.length;
|
|
7026
8116
|
const pointCount = transformed.mode === 'gauge'
|
|
7027
8117
|
? transformed.gauge ? 1 : 0
|
|
7028
|
-
: transformed.
|
|
7029
|
-
? transformed.
|
|
7030
|
-
: transformed.
|
|
8118
|
+
: transformed.mode === 'heatmap'
|
|
8119
|
+
? transformed.heatmap?.cells.length ?? 0
|
|
8120
|
+
: transformed.slices.length
|
|
8121
|
+
? transformed.slices.length
|
|
8122
|
+
: transformed.series.reduce((total, series) => total + series.points.length, 0);
|
|
7031
8123
|
const params = {
|
|
7032
8124
|
title,
|
|
7033
8125
|
type: this.chartTypeLabel(config.type),
|
|
@@ -7061,6 +8153,7 @@ class EChartsOptionBuilderService {
|
|
|
7061
8153
|
pyramid: 'Pyramid',
|
|
7062
8154
|
treemap: 'Treemap',
|
|
7063
8155
|
gauge: 'Gauge',
|
|
8156
|
+
heatmap: 'Heatmap',
|
|
7064
8157
|
};
|
|
7065
8158
|
return this.t(`praxis.charts.runtime.accessibility.chartType.${type}`, fallbacks[type]);
|
|
7066
8159
|
}
|
|
@@ -7070,6 +8163,24 @@ class EChartsOptionBuilderService {
|
|
|
7070
8163
|
resolveTextColor(config) {
|
|
7071
8164
|
return config.theme?.textColor ?? '#4b5563';
|
|
7072
8165
|
}
|
|
8166
|
+
isLightTextColor(color) {
|
|
8167
|
+
const trimmed = String(color || '').trim();
|
|
8168
|
+
const hex6 = /^#([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})$/.exec(trimmed);
|
|
8169
|
+
if (hex6) {
|
|
8170
|
+
const r = Number.parseInt(hex6[1], 16) / 255;
|
|
8171
|
+
const g = Number.parseInt(hex6[2], 16) / 255;
|
|
8172
|
+
const b = Number.parseInt(hex6[3], 16) / 255;
|
|
8173
|
+
return 0.2126 * r + 0.7152 * g + 0.0722 * b > 0.6;
|
|
8174
|
+
}
|
|
8175
|
+
const hex3 = /^#([0-9a-fA-F])([0-9a-fA-F])([0-9a-fA-F])$/.exec(trimmed);
|
|
8176
|
+
if (hex3) {
|
|
8177
|
+
const r = Number.parseInt(hex3[1] + hex3[1], 16) / 255;
|
|
8178
|
+
const g = Number.parseInt(hex3[2] + hex3[2], 16) / 255;
|
|
8179
|
+
const b = Number.parseInt(hex3[3] + hex3[3], 16) / 255;
|
|
8180
|
+
return 0.2126 * r + 0.7152 * g + 0.0722 * b > 0.6;
|
|
8181
|
+
}
|
|
8182
|
+
return false;
|
|
8183
|
+
}
|
|
7073
8184
|
hasText(value) {
|
|
7074
8185
|
const text = this.resolveText(value);
|
|
7075
8186
|
return !!text && text.trim().length > 0;
|
|
@@ -7203,24 +8314,106 @@ class EChartsOptionBuilderService {
|
|
|
7203
8314
|
},
|
|
7204
8315
|
];
|
|
7205
8316
|
}
|
|
7206
|
-
primaryValueFormat(config) {
|
|
7207
|
-
return config.series.find((series) => series.axis !== 'secondary')?.labels?.format
|
|
7208
|
-
?? config.axes?.y?.labels?.format
|
|
7209
|
-
?? config.series[0]?.labels?.format;
|
|
8317
|
+
primaryValueFormat(config) {
|
|
8318
|
+
return config.series.find((series) => series.axis !== 'secondary')?.labels?.format
|
|
8319
|
+
?? config.axes?.y?.labels?.format
|
|
8320
|
+
?? config.series[0]?.labels?.format;
|
|
8321
|
+
}
|
|
8322
|
+
buildCartesianTooltip(config) {
|
|
8323
|
+
const hasQuickView = hasChartQuickViewBinding(config);
|
|
8324
|
+
const tooltip = {
|
|
8325
|
+
trigger: config.theme?.tooltip?.trigger ?? 'axis',
|
|
8326
|
+
confine: true,
|
|
8327
|
+
appendToBody: false,
|
|
8328
|
+
valueFormatter: (value) => this.formatValue(value, this.primaryValueFormat(config)),
|
|
8329
|
+
};
|
|
8330
|
+
if (this.usesCanonicalStatsTime(config)) {
|
|
8331
|
+
return {
|
|
8332
|
+
...tooltip,
|
|
8333
|
+
formatter: (params) => {
|
|
8334
|
+
if (hasQuickView && this.shouldSuppressTooltipForParams(params, config)) {
|
|
8335
|
+
return '';
|
|
8336
|
+
}
|
|
8337
|
+
return this.formatCanonicalTimeTooltip(params, config);
|
|
8338
|
+
},
|
|
8339
|
+
};
|
|
8340
|
+
}
|
|
8341
|
+
if (hasQuickView) {
|
|
8342
|
+
return {
|
|
8343
|
+
...tooltip,
|
|
8344
|
+
formatter: (params) => {
|
|
8345
|
+
if (this.shouldSuppressTooltipForParams(params, config)) {
|
|
8346
|
+
return '';
|
|
8347
|
+
}
|
|
8348
|
+
return this.formatDefaultCartesianTooltip(params, config);
|
|
8349
|
+
},
|
|
8350
|
+
};
|
|
8351
|
+
}
|
|
8352
|
+
return tooltip;
|
|
8353
|
+
}
|
|
8354
|
+
pointTooltipSuppression(config, seriesId, seriesName, data) {
|
|
8355
|
+
const eligible = resolveChartPointQuickView(config, {
|
|
8356
|
+
seriesId,
|
|
8357
|
+
seriesName,
|
|
8358
|
+
data,
|
|
8359
|
+
});
|
|
8360
|
+
return eligible ? { tooltip: { show: false } } : undefined;
|
|
7210
8361
|
}
|
|
7211
|
-
|
|
7212
|
-
|
|
7213
|
-
|
|
7214
|
-
|
|
7215
|
-
|
|
7216
|
-
|
|
8362
|
+
decorateSeriesPointWithQuickViewTooltipSuppression(config, seriesId, seriesName, point) {
|
|
8363
|
+
if (!point || typeof point !== 'object' || Array.isArray(point)) {
|
|
8364
|
+
return point;
|
|
8365
|
+
}
|
|
8366
|
+
const suppression = this.pointTooltipSuppression(config, seriesId, seriesName, point);
|
|
8367
|
+
if (!suppression) {
|
|
8368
|
+
return point;
|
|
8369
|
+
}
|
|
8370
|
+
return {
|
|
8371
|
+
...point,
|
|
8372
|
+
...suppression,
|
|
7217
8373
|
};
|
|
7218
|
-
|
|
7219
|
-
|
|
7220
|
-
|
|
7221
|
-
|
|
7222
|
-
|
|
7223
|
-
|
|
8374
|
+
}
|
|
8375
|
+
shouldSuppressTooltipForParams(params, config) {
|
|
8376
|
+
const items = (Array.isArray(params) ? params : [params])
|
|
8377
|
+
.filter((item) => !!item && typeof item === 'object');
|
|
8378
|
+
if (!items.length) {
|
|
8379
|
+
return false;
|
|
8380
|
+
}
|
|
8381
|
+
return items.some((item) => {
|
|
8382
|
+
const data = this.tooltipPointData(item['data']);
|
|
8383
|
+
const seriesId = item['seriesId'] == null ? undefined : String(item['seriesId']);
|
|
8384
|
+
const seriesName = item['seriesName'] == null ? undefined : String(item['seriesName']);
|
|
8385
|
+
return resolveChartPointQuickView(config, {
|
|
8386
|
+
seriesId,
|
|
8387
|
+
seriesName,
|
|
8388
|
+
data,
|
|
8389
|
+
}) !== null;
|
|
8390
|
+
});
|
|
8391
|
+
}
|
|
8392
|
+
formatDefaultCartesianTooltip(params, config) {
|
|
8393
|
+
const items = (Array.isArray(params) ? params : [params])
|
|
8394
|
+
.filter((item) => !!item && typeof item === 'object');
|
|
8395
|
+
if (!items.length) {
|
|
8396
|
+
return '';
|
|
8397
|
+
}
|
|
8398
|
+
const first = items[0];
|
|
8399
|
+
const header = this.nonEmptyText(first['axisValueLabel'])
|
|
8400
|
+
?? this.nonEmptyText(first['name'])
|
|
8401
|
+
?? '';
|
|
8402
|
+
const lines = items.map((item) => {
|
|
8403
|
+
const itemData = this.tooltipPointData(item['data']);
|
|
8404
|
+
const seriesId = item['seriesId'] == null ? undefined : String(item['seriesId']);
|
|
8405
|
+
const format = seriesId
|
|
8406
|
+
? this.seriesLabelFormat(config, seriesId) ?? this.primaryValueFormat(config)
|
|
8407
|
+
: this.primaryValueFormat(config);
|
|
8408
|
+
const value = this.formatValue(item['value'] ?? itemData['value'], format);
|
|
8409
|
+
const seriesName = this.nonEmptyText(item['seriesName']);
|
|
8410
|
+
return seriesName
|
|
8411
|
+
? `${escapeTooltipHtml(seriesName)}: ${escapeTooltipHtml(value)}`
|
|
8412
|
+
: escapeTooltipHtml(value);
|
|
8413
|
+
});
|
|
8414
|
+
return [escapeTooltipHtml(header), ...lines]
|
|
8415
|
+
.filter((part) => part.length > 0)
|
|
8416
|
+
.join('<br/>');
|
|
7224
8417
|
}
|
|
7225
8418
|
usesCanonicalStatsTime(config) {
|
|
7226
8419
|
if (config.dataSource?.kind !== 'remote'
|
|
@@ -7282,6 +8475,37 @@ class EChartsOptionBuilderService {
|
|
|
7282
8475
|
? value
|
|
7283
8476
|
: {};
|
|
7284
8477
|
}
|
|
8478
|
+
formatHeatmapTooltip(params, heatmap, format) {
|
|
8479
|
+
const item = Array.isArray(params) ? params[0] : params;
|
|
8480
|
+
if (!item || typeof item !== 'object') {
|
|
8481
|
+
return '';
|
|
8482
|
+
}
|
|
8483
|
+
const record = item;
|
|
8484
|
+
const data = this.tooltipPointData(record['data']);
|
|
8485
|
+
const rawTuple = Array.isArray(record['value'])
|
|
8486
|
+
? record['value']
|
|
8487
|
+
: (Array.isArray(data['value']) ? data['value'] : []);
|
|
8488
|
+
const xIdx = Number(rawTuple[0]);
|
|
8489
|
+
const yIdx = Number(rawTuple[1]);
|
|
8490
|
+
const rawVal = rawTuple.length >= 3 ? rawTuple[2] : record['value'];
|
|
8491
|
+
const xLabel = this.nonEmptyText(data['category'])
|
|
8492
|
+
?? (Number.isInteger(xIdx) ? heatmap.xCategories[xIdx] : undefined)
|
|
8493
|
+
?? '';
|
|
8494
|
+
const yLabel = this.nonEmptyText(data['secondaryCategory'])
|
|
8495
|
+
?? (Number.isInteger(yIdx) ? heatmap.yCategories[yIdx] : undefined)
|
|
8496
|
+
?? '';
|
|
8497
|
+
const header = [xLabel, yLabel].filter(Boolean).join(' • ')
|
|
8498
|
+
|| this.nonEmptyText(record['name'])
|
|
8499
|
+
|| '';
|
|
8500
|
+
const valueText = rawVal === null || rawVal === undefined
|
|
8501
|
+
? this.t('praxis.charts.runtime.accessibility.notAvailable', 'Not available')
|
|
8502
|
+
: this.formatValue(rawVal, format);
|
|
8503
|
+
const seriesName = this.nonEmptyText(record['seriesName']) ?? heatmap.name;
|
|
8504
|
+
const line = seriesName
|
|
8505
|
+
? `${escapeTooltipHtml(seriesName)}: ${escapeTooltipHtml(valueText)}`
|
|
8506
|
+
: escapeTooltipHtml(valueText);
|
|
8507
|
+
return [escapeTooltipHtml(header), line].filter(Boolean).join('<br/>');
|
|
8508
|
+
}
|
|
7285
8509
|
nonEmptyText(value) {
|
|
7286
8510
|
if (value === null || value === undefined) {
|
|
7287
8511
|
return undefined;
|
|
@@ -7544,6 +8768,7 @@ use([
|
|
|
7544
8768
|
FunnelChart,
|
|
7545
8769
|
GaugeChart,
|
|
7546
8770
|
GridComponent,
|
|
8771
|
+
HeatmapChart,
|
|
7547
8772
|
LegendComponent,
|
|
7548
8773
|
LineChart,
|
|
7549
8774
|
PieChart,
|
|
@@ -7554,6 +8779,7 @@ use([
|
|
|
7554
8779
|
TransformComponent,
|
|
7555
8780
|
TreemapChart,
|
|
7556
8781
|
UniversalTransition,
|
|
8782
|
+
VisualMapComponent,
|
|
7557
8783
|
]);
|
|
7558
8784
|
class EChartsEngineAdapter {
|
|
7559
8785
|
optionBuilder;
|
|
@@ -7589,6 +8815,9 @@ class EChartsEngineAdapter {
|
|
|
7589
8815
|
this.lastStructuralSignature = nextStructuralSignature;
|
|
7590
8816
|
this.detachHandlers();
|
|
7591
8817
|
chart.off('click');
|
|
8818
|
+
chart.off('mouseover');
|
|
8819
|
+
chart.off('mouseout');
|
|
8820
|
+
chart.off('globalout');
|
|
7592
8821
|
chart.on('click', (params) => {
|
|
7593
8822
|
this.lastSeriesClickAt = Date.now();
|
|
7594
8823
|
this.lastGridClick = undefined;
|
|
@@ -7602,6 +8831,45 @@ class EChartsEngineAdapter {
|
|
|
7602
8831
|
data,
|
|
7603
8832
|
});
|
|
7604
8833
|
});
|
|
8834
|
+
chart.on('mouseover', (params) => {
|
|
8835
|
+
const data = pointData(params?.name, params?.data ?? params?.value);
|
|
8836
|
+
const category = this.resolvePointCategory(payload, data, params?.name);
|
|
8837
|
+
const quickViewTarget = resolveChartPointQuickView(payload.config, {
|
|
8838
|
+
seriesId: params?.seriesId,
|
|
8839
|
+
seriesName: params?.seriesName,
|
|
8840
|
+
data,
|
|
8841
|
+
});
|
|
8842
|
+
if (quickViewTarget) {
|
|
8843
|
+
chart.dispatchAction?.({ type: 'hideTip' });
|
|
8844
|
+
}
|
|
8845
|
+
const anchorKey = `${payload.config.id ?? 'chart'}:${params?.seriesId ?? params?.seriesIndex ?? 0}:${params?.dataIndex ?? category ?? ''}`;
|
|
8846
|
+
payload.onPointHoverEnter?.({
|
|
8847
|
+
chartId: payload.config.id,
|
|
8848
|
+
seriesId: params?.seriesId,
|
|
8849
|
+
seriesName: params?.seriesName,
|
|
8850
|
+
category,
|
|
8851
|
+
value: pointValue(params?.value ?? params?.data),
|
|
8852
|
+
data,
|
|
8853
|
+
anchorKey,
|
|
8854
|
+
getViewportRect: () => this.resolvePointViewportRect(host, chart, params),
|
|
8855
|
+
});
|
|
8856
|
+
});
|
|
8857
|
+
chart.on('mouseout', (params) => {
|
|
8858
|
+
const data = pointData(params?.name, params?.data ?? params?.value);
|
|
8859
|
+
payload.onPointHoverLeave?.({
|
|
8860
|
+
chartId: payload.config.id,
|
|
8861
|
+
seriesId: params?.seriesId,
|
|
8862
|
+
seriesName: params?.seriesName,
|
|
8863
|
+
category: this.resolvePointCategory(payload, data, params?.name),
|
|
8864
|
+
value: pointValue(params?.value ?? params?.data),
|
|
8865
|
+
data,
|
|
8866
|
+
});
|
|
8867
|
+
});
|
|
8868
|
+
chart.on('globalout', () => {
|
|
8869
|
+
payload.onPointHoverLeave?.({
|
|
8870
|
+
chartId: payload.config.id,
|
|
8871
|
+
});
|
|
8872
|
+
});
|
|
7605
8873
|
const zrender = chart?.getZr?.();
|
|
7606
8874
|
this.zrenderClickHandler = (event) => {
|
|
7607
8875
|
const occurredAt = Date.now();
|
|
@@ -7643,6 +8911,9 @@ class EChartsEngineAdapter {
|
|
|
7643
8911
|
disposeChart() {
|
|
7644
8912
|
this.detachHandlers();
|
|
7645
8913
|
this.chart?.off('click');
|
|
8914
|
+
this.chart?.off('mouseover');
|
|
8915
|
+
this.chart?.off('mouseout');
|
|
8916
|
+
this.chart?.off('globalout');
|
|
7646
8917
|
this.chart?.dispose();
|
|
7647
8918
|
this.chart = undefined;
|
|
7648
8919
|
this.currentHost = undefined;
|
|
@@ -7650,6 +8921,111 @@ class EChartsEngineAdapter {
|
|
|
7650
8921
|
this.lastSeriesClickAt = 0;
|
|
7651
8922
|
this.lastGridClick = undefined;
|
|
7652
8923
|
}
|
|
8924
|
+
resolvePointViewportRect(host, chart, params) {
|
|
8925
|
+
if (!host.isConnected) {
|
|
8926
|
+
return null;
|
|
8927
|
+
}
|
|
8928
|
+
const hostRect = host.getBoundingClientRect();
|
|
8929
|
+
if (hostRect.width <= 0 || hostRect.height <= 0) {
|
|
8930
|
+
return null;
|
|
8931
|
+
}
|
|
8932
|
+
const target = params?.event?.target;
|
|
8933
|
+
if (target && typeof target.getBoundingRect === 'function') {
|
|
8934
|
+
try {
|
|
8935
|
+
const rawRect = target.getBoundingRect();
|
|
8936
|
+
if (rawRect && Number.isFinite(rawRect.x) && Number.isFinite(rawRect.y)) {
|
|
8937
|
+
let localLeft = Number(rawRect.x);
|
|
8938
|
+
let localTop = Number(rawRect.y);
|
|
8939
|
+
let localWidth = Math.max(8, Number(rawRect.width) || 12);
|
|
8940
|
+
let localHeight = Math.max(8, Number(rawRect.height) || 12);
|
|
8941
|
+
if (typeof target.transformCoordToGlobal === 'function') {
|
|
8942
|
+
const p1 = target.transformCoordToGlobal(rawRect.x, rawRect.y);
|
|
8943
|
+
const p2 = target.transformCoordToGlobal(rawRect.x + (Number(rawRect.width) || 0), rawRect.y + (Number(rawRect.height) || 0));
|
|
8944
|
+
if (Array.isArray(p1)
|
|
8945
|
+
&& Array.isArray(p2)
|
|
8946
|
+
&& Number.isFinite(p1[0])
|
|
8947
|
+
&& Number.isFinite(p1[1])
|
|
8948
|
+
&& Number.isFinite(p2[0])
|
|
8949
|
+
&& Number.isFinite(p2[1])) {
|
|
8950
|
+
localLeft = Math.min(p1[0], p2[0]);
|
|
8951
|
+
localTop = Math.min(p1[1], p2[1]);
|
|
8952
|
+
localWidth = Math.max(8, Math.abs(p2[0] - p1[0]));
|
|
8953
|
+
localHeight = Math.max(8, Math.abs(p2[1] - p1[1]));
|
|
8954
|
+
}
|
|
8955
|
+
}
|
|
8956
|
+
if (localWidth < hostRect.width || localHeight < hostRect.height) {
|
|
8957
|
+
const x = Math.round(hostRect.left + localLeft);
|
|
8958
|
+
const y = Math.round(hostRect.top + localTop);
|
|
8959
|
+
return {
|
|
8960
|
+
x,
|
|
8961
|
+
y,
|
|
8962
|
+
left: x,
|
|
8963
|
+
top: y,
|
|
8964
|
+
width: Math.round(localWidth),
|
|
8965
|
+
height: Math.round(localHeight),
|
|
8966
|
+
};
|
|
8967
|
+
}
|
|
8968
|
+
}
|
|
8969
|
+
}
|
|
8970
|
+
catch {
|
|
8971
|
+
// Fallback to pixel conversion / event offset below.
|
|
8972
|
+
}
|
|
8973
|
+
}
|
|
8974
|
+
const anyChart = chart;
|
|
8975
|
+
if (typeof anyChart?.convertToPixel === 'function') {
|
|
8976
|
+
try {
|
|
8977
|
+
const seriesIndex = Number.isInteger(params?.seriesIndex) ? params.seriesIndex : 0;
|
|
8978
|
+
const coordInput = Array.isArray(params?.value)
|
|
8979
|
+
? params.value
|
|
8980
|
+
: Number.isInteger(params?.dataIndex)
|
|
8981
|
+
? [params.dataIndex, pointValue(params?.value ?? params?.data)]
|
|
8982
|
+
: undefined;
|
|
8983
|
+
if (coordInput !== undefined) {
|
|
8984
|
+
const pixel = anyChart.convertToPixel({ seriesIndex }, coordInput)
|
|
8985
|
+
?? anyChart.convertToPixel({ gridIndex: 0 }, coordInput);
|
|
8986
|
+
if (Array.isArray(pixel) && Number.isFinite(pixel[0]) && Number.isFinite(pixel[1])) {
|
|
8987
|
+
const x = Math.round(hostRect.left + Number(pixel[0]) - 6);
|
|
8988
|
+
const y = Math.round(hostRect.top + Number(pixel[1]) - 6);
|
|
8989
|
+
return {
|
|
8990
|
+
x,
|
|
8991
|
+
y,
|
|
8992
|
+
left: x,
|
|
8993
|
+
top: y,
|
|
8994
|
+
width: 12,
|
|
8995
|
+
height: 12,
|
|
8996
|
+
};
|
|
8997
|
+
}
|
|
8998
|
+
}
|
|
8999
|
+
}
|
|
9000
|
+
catch {
|
|
9001
|
+
// Fallback to pointer offset below.
|
|
9002
|
+
}
|
|
9003
|
+
}
|
|
9004
|
+
const offsetX = Number(params?.event?.offsetX);
|
|
9005
|
+
const offsetY = Number(params?.event?.offsetY);
|
|
9006
|
+
if (Number.isFinite(offsetX) && Number.isFinite(offsetY)) {
|
|
9007
|
+
const x = Math.round(hostRect.left + offsetX - 6);
|
|
9008
|
+
const y = Math.round(hostRect.top + offsetY - 6);
|
|
9009
|
+
return {
|
|
9010
|
+
x,
|
|
9011
|
+
y,
|
|
9012
|
+
left: x,
|
|
9013
|
+
top: y,
|
|
9014
|
+
width: 12,
|
|
9015
|
+
height: 12,
|
|
9016
|
+
};
|
|
9017
|
+
}
|
|
9018
|
+
const fallbackX = Math.round(hostRect.left + Math.min(24, Math.max(8, hostRect.width / 2)));
|
|
9019
|
+
const fallbackY = Math.round(hostRect.top + Math.min(24, Math.max(8, hostRect.height / 2)));
|
|
9020
|
+
return {
|
|
9021
|
+
x: fallbackX,
|
|
9022
|
+
y: fallbackY,
|
|
9023
|
+
left: fallbackX,
|
|
9024
|
+
top: fallbackY,
|
|
9025
|
+
width: 16,
|
|
9026
|
+
height: 16,
|
|
9027
|
+
};
|
|
9028
|
+
}
|
|
7653
9029
|
resolveStructuralSignature(config) {
|
|
7654
9030
|
const mode = config.type === 'pie' || config.type === 'donut'
|
|
7655
9031
|
? 'pie'
|
|
@@ -7659,9 +9035,11 @@ class EChartsEngineAdapter {
|
|
|
7659
9035
|
? 'treemap'
|
|
7660
9036
|
: config.type === 'gauge'
|
|
7661
9037
|
? 'gauge'
|
|
7662
|
-
: config.type === '
|
|
7663
|
-
? '
|
|
7664
|
-
: '
|
|
9038
|
+
: config.type === 'heatmap'
|
|
9039
|
+
? 'heatmap'
|
|
9040
|
+
: config.type === 'scatter'
|
|
9041
|
+
? 'scatter'
|
|
9042
|
+
: 'cartesian';
|
|
7665
9043
|
const orientation = config.orientation === 'horizontal' || config.type === 'horizontal-bar'
|
|
7666
9044
|
? 'horizontal'
|
|
7667
9045
|
: 'vertical';
|
|
@@ -7732,7 +9110,64 @@ class EChartsEngineAdapter {
|
|
|
7732
9110
|
const option = chart.getOption?.();
|
|
7733
9111
|
const xAxis = firstOptionEntry(option?.xAxis);
|
|
7734
9112
|
const yAxis = firstOptionEntry(option?.yAxis);
|
|
7735
|
-
const
|
|
9113
|
+
const isTwoDimensionalCategoryGrid = xAxis?.type === 'category' && yAxis?.type === 'category';
|
|
9114
|
+
if (isTwoDimensionalCategoryGrid) {
|
|
9115
|
+
const xCategories = Array.isArray(xAxis?.data) ? xAxis.data : [];
|
|
9116
|
+
const yCategories = Array.isArray(yAxis?.data) ? yAxis.data : [];
|
|
9117
|
+
if (!xCategories.length || !yCategories.length) {
|
|
9118
|
+
return;
|
|
9119
|
+
}
|
|
9120
|
+
const converted = chart.convertFromPixel({ gridIndex: 0 }, point);
|
|
9121
|
+
const xIndex = Math.round(Number(converted?.[0]));
|
|
9122
|
+
const yIndex = Math.round(Number(converted?.[1]));
|
|
9123
|
+
if (!Number.isInteger(xIndex)
|
|
9124
|
+
|| xIndex < 0
|
|
9125
|
+
|| xIndex >= xCategories.length
|
|
9126
|
+
|| !Number.isInteger(yIndex)
|
|
9127
|
+
|| yIndex < 0
|
|
9128
|
+
|| yIndex >= yCategories.length) {
|
|
9129
|
+
return;
|
|
9130
|
+
}
|
|
9131
|
+
const xCategoryValue = xCategories[xIndex] == null ? undefined : String(xCategories[xIndex]);
|
|
9132
|
+
const yCategoryValue = yCategories[yIndex] == null ? undefined : String(yCategories[yIndex]);
|
|
9133
|
+
const categoryField = payload.config.axes?.x?.field
|
|
9134
|
+
?? payload.config.series.find((series) => series.categoryField)?.categoryField;
|
|
9135
|
+
const secondaryField = payload.config.series.find((series) => series.secondaryCategoryField)?.secondaryCategoryField
|
|
9136
|
+
?? (payload.config.axes?.y?.type === 'category' ? payload.config.axes.y.field : undefined);
|
|
9137
|
+
const signature = JSON.stringify({
|
|
9138
|
+
chartId: payload.config.id,
|
|
9139
|
+
category: xCategoryValue,
|
|
9140
|
+
secondaryCategory: yCategoryValue,
|
|
9141
|
+
xIndex,
|
|
9142
|
+
yIndex,
|
|
9143
|
+
});
|
|
9144
|
+
if (this.isDuplicateGridClick(signature, occurredAt)) {
|
|
9145
|
+
return;
|
|
9146
|
+
}
|
|
9147
|
+
const sourceRow = payload.data.find((row) => {
|
|
9148
|
+
const rowX = String((categoryField ? row[categoryField] : undefined) ?? row['label'] ?? '');
|
|
9149
|
+
const rowY = String((secondaryField ? row[secondaryField] : undefined) ?? row['secondaryLabel'] ?? '');
|
|
9150
|
+
return rowX === (xCategoryValue ?? '') && rowY === (yCategoryValue ?? '');
|
|
9151
|
+
});
|
|
9152
|
+
const data = {
|
|
9153
|
+
...(sourceRow ?? {}),
|
|
9154
|
+
category: xCategoryValue,
|
|
9155
|
+
secondaryCategory: yCategoryValue,
|
|
9156
|
+
};
|
|
9157
|
+
if (categoryField && xCategoryValue !== undefined && data[categoryField] === undefined) {
|
|
9158
|
+
data[categoryField] = xCategoryValue;
|
|
9159
|
+
}
|
|
9160
|
+
if (secondaryField && yCategoryValue !== undefined && data[secondaryField] === undefined) {
|
|
9161
|
+
data[secondaryField] = yCategoryValue;
|
|
9162
|
+
}
|
|
9163
|
+
payload.onPointClick?.({
|
|
9164
|
+
chartId: payload.config.id,
|
|
9165
|
+
category: xCategoryValue,
|
|
9166
|
+
data,
|
|
9167
|
+
});
|
|
9168
|
+
return;
|
|
9169
|
+
}
|
|
9170
|
+
const horizontal = yAxis?.type === 'category' && xAxis?.type !== 'category';
|
|
7736
9171
|
const categories = (horizontal ? yAxis?.data : xAxis?.data) ?? [];
|
|
7737
9172
|
if (!Array.isArray(categories) || categories.length === 0) {
|
|
7738
9173
|
return;
|
|
@@ -7799,7 +9234,7 @@ function firstOptionEntry(value) {
|
|
|
7799
9234
|
}
|
|
7800
9235
|
function pointValue(value) {
|
|
7801
9236
|
if (Array.isArray(value)) {
|
|
7802
|
-
return value.length > 1 ? value[1] : value[0];
|
|
9237
|
+
return value.length > 1 ? value[value.length - 1] : value[0];
|
|
7803
9238
|
}
|
|
7804
9239
|
if (value && typeof value === 'object' && 'value' in value) {
|
|
7805
9240
|
return pointValue(value.value);
|
|
@@ -9696,6 +11131,20 @@ function resolveShowcaseStateMode(config, remoteRuntimeState = 'idle') {
|
|
|
9696
11131
|
}
|
|
9697
11132
|
return 'ready';
|
|
9698
11133
|
}
|
|
11134
|
+
const INTERNAL_CHART_ROW_FIELDS = new Set([
|
|
11135
|
+
'key',
|
|
11136
|
+
'label',
|
|
11137
|
+
'secondaryKey',
|
|
11138
|
+
'secondaryLabel',
|
|
11139
|
+
'compositeKey',
|
|
11140
|
+
'coordinates',
|
|
11141
|
+
'value',
|
|
11142
|
+
'count',
|
|
11143
|
+
'from',
|
|
11144
|
+
'to',
|
|
11145
|
+
'mode',
|
|
11146
|
+
'requestMode',
|
|
11147
|
+
]);
|
|
9699
11148
|
function buildTableConfig(config, rows) {
|
|
9700
11149
|
const tableConfig = createDefaultTableConfig();
|
|
9701
11150
|
const toolbar = tableConfig.toolbar;
|
|
@@ -9711,6 +11160,7 @@ function buildTableConfig(config, rows) {
|
|
|
9711
11160
|
const appearance = tableConfig.appearance;
|
|
9712
11161
|
const categoryField = config.axes?.x?.field || config.series[0]?.categoryField;
|
|
9713
11162
|
const categoryLabel = config.axes?.x?.label || categoryField;
|
|
11163
|
+
const secondaryCategoryField = config.series[0]?.secondaryCategoryField?.trim();
|
|
9714
11164
|
const sampleRow = rows[0] || {};
|
|
9715
11165
|
const seriesColumns = config.series
|
|
9716
11166
|
.map((series) => {
|
|
@@ -9724,7 +11174,9 @@ function buildTableConfig(config, rows) {
|
|
|
9724
11174
|
};
|
|
9725
11175
|
})
|
|
9726
11176
|
.filter((column) => column !== null);
|
|
9727
|
-
const fallbackColumns = Object.keys(sampleRow)
|
|
11177
|
+
const fallbackColumns = Object.keys(sampleRow)
|
|
11178
|
+
.filter((field) => !INTERNAL_CHART_ROW_FIELDS.has(field))
|
|
11179
|
+
.map((field) => ({
|
|
9728
11180
|
field,
|
|
9729
11181
|
header: field,
|
|
9730
11182
|
}));
|
|
@@ -9734,7 +11186,18 @@ function buildTableConfig(config, rows) {
|
|
|
9734
11186
|
header: categoryLabel || categoryField,
|
|
9735
11187
|
}]
|
|
9736
11188
|
: [];
|
|
9737
|
-
const
|
|
11189
|
+
const secondaryCategoryColumn = secondaryCategoryField
|
|
11190
|
+
? [{
|
|
11191
|
+
field: secondaryCategoryField,
|
|
11192
|
+
header: secondaryCategoryField,
|
|
11193
|
+
}]
|
|
11194
|
+
: [];
|
|
11195
|
+
const dedupedColumns = [
|
|
11196
|
+
...categoryColumn,
|
|
11197
|
+
...secondaryCategoryColumn,
|
|
11198
|
+
...seriesColumns,
|
|
11199
|
+
...fallbackColumns,
|
|
11200
|
+
].filter((column, index, source) => source.findIndex((candidate) => candidate.field === column.field) === index);
|
|
9738
11201
|
tableConfig.columns = dedupedColumns;
|
|
9739
11202
|
toolbar.visible = false;
|
|
9740
11203
|
pagination.enabled = rows.length > 10;
|
|
@@ -10133,6 +11596,7 @@ const PRAXIS_CHARTS_EN_US = {
|
|
|
10133
11596
|
'praxis.charts.runtime.accessibility.chartType.pyramid': 'Pyramid',
|
|
10134
11597
|
'praxis.charts.runtime.accessibility.chartType.treemap': 'Treemap',
|
|
10135
11598
|
'praxis.charts.runtime.accessibility.chartType.gauge': 'Gauge',
|
|
11599
|
+
'praxis.charts.runtime.accessibility.chartType.heatmap': 'Heatmap',
|
|
10136
11600
|
'praxis.charts.runtime.comparisonCurrent': 'Current',
|
|
10137
11601
|
'praxis.charts.runtime.comparisonPrevious': 'Previous',
|
|
10138
11602
|
'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.',
|
|
@@ -10173,6 +11637,7 @@ const PRAXIS_CHARTS_EN_US = {
|
|
|
10173
11637
|
'praxis.charts.editor.field.comparisonMode': 'Comparison mode',
|
|
10174
11638
|
'praxis.charts.editor.field.dimension': 'Dimension',
|
|
10175
11639
|
'praxis.charts.editor.field.dimensionRole': 'Dimension role',
|
|
11640
|
+
'praxis.charts.editor.field.dimensionExtraction': 'Temporal extraction',
|
|
10176
11641
|
'praxis.charts.editor.field.metric': 'Metric',
|
|
10177
11642
|
'praxis.charts.editor.field.metricLabel': 'Metric label',
|
|
10178
11643
|
'praxis.charts.editor.field.metricAggregation': 'Aggregation',
|
|
@@ -10326,6 +11791,8 @@ const PRAXIS_CHARTS_EN_US = {
|
|
|
10326
11791
|
'praxis.charts.editor.specialization.scatterHint': 'With one metric, scatter uses the first dimension as X and the metric as Y. With two metrics, the dimension identifies the grouped bucket and the metrics map to X and Y.',
|
|
10327
11792
|
'praxis.charts.editor.specialization.gaugeTitle': 'Gauge scale',
|
|
10328
11793
|
'praxis.charts.editor.specialization.gaugeHint': 'Gauge charts render exactly one bucket and one metric. Remote stats use group-by with limit 1; values outside the scale fail instead of being clamped.',
|
|
11794
|
+
'praxis.charts.editor.specialization.heatmapTitle': 'Heatmap matrix guidance',
|
|
11795
|
+
'praxis.charts.editor.specialization.heatmapHint': 'Heatmap charts render a 2D matrix requiring two dimensions (X and Y axes) and one metric, using group-by for remote stats.',
|
|
10329
11796
|
'praxis.charts.editor.events.pointClickTitle': 'Point click',
|
|
10330
11797
|
'praxis.charts.editor.events.selectionChangeTitle': 'Selection change',
|
|
10331
11798
|
'praxis.charts.editor.events.drillDownTitle': 'Drill down',
|
|
@@ -10368,10 +11835,16 @@ const PRAXIS_CHARTS_EN_US = {
|
|
|
10368
11835
|
'praxis.charts.editor.dimensionRole.segment': 'segment',
|
|
10369
11836
|
'praxis.charts.editor.dimensionRole.time': 'time',
|
|
10370
11837
|
'praxis.charts.editor.dimensionRole.value': 'value',
|
|
11838
|
+
'praxis.charts.editor.dimensionExtraction.none': 'None',
|
|
11839
|
+
'praxis.charts.editor.dimensionExtraction.HOUR_OF_DAY': 'Hour of day',
|
|
11840
|
+
'praxis.charts.editor.dimensionExtraction.DAY_OF_WEEK': 'Day of week',
|
|
11841
|
+
'praxis.charts.editor.dimensionExtraction.DAY_OF_MONTH': 'Day of month',
|
|
11842
|
+
'praxis.charts.editor.dimensionExtraction.MONTH_OF_YEAR': 'Month of year',
|
|
10371
11843
|
'praxis.charts.editor.metricAggregation.sum': 'sum',
|
|
10372
11844
|
'praxis.charts.editor.metricAggregation.count': 'count',
|
|
10373
11845
|
'praxis.charts.editor.metricAggregation.distinct-count': 'distinct count',
|
|
10374
11846
|
'praxis.charts.editor.metricAggregation.avg': 'avg',
|
|
11847
|
+
'praxis.charts.editor.metricAggregation.avg-per-cycle-occurrence': 'avg per cycle occurrence',
|
|
10375
11848
|
'praxis.charts.editor.metricAggregation.min': 'min',
|
|
10376
11849
|
'praxis.charts.editor.metricAggregation.max': 'max',
|
|
10377
11850
|
'praxis.charts.editor.metricAxis.primary': 'primary',
|
|
@@ -10398,6 +11871,7 @@ const PRAXIS_CHARTS_EN_US = {
|
|
|
10398
11871
|
'praxis.charts.editor.kind.pyramid': 'Pyramid',
|
|
10399
11872
|
'praxis.charts.editor.kind.treemap': 'Treemap',
|
|
10400
11873
|
'praxis.charts.editor.kind.gauge': 'Gauge',
|
|
11874
|
+
'praxis.charts.editor.kind.heatmap': 'Heatmap',
|
|
10401
11875
|
'praxis.charts.analyticsPresentation.selectorLabel': 'Analytics presentation',
|
|
10402
11876
|
'praxis.charts.analyticsPresentation.chart': 'Chart',
|
|
10403
11877
|
'praxis.charts.analyticsPresentation.table': 'Table',
|
|
@@ -10462,6 +11936,7 @@ const PRAXIS_CHARTS_PT_BR = {
|
|
|
10462
11936
|
'praxis.charts.runtime.accessibility.chartType.pyramid': 'Pirâmide',
|
|
10463
11937
|
'praxis.charts.runtime.accessibility.chartType.treemap': 'Mapa de árvore',
|
|
10464
11938
|
'praxis.charts.runtime.accessibility.chartType.gauge': 'Velocímetro',
|
|
11939
|
+
'praxis.charts.runtime.accessibility.chartType.heatmap': 'Mapa de calor',
|
|
10465
11940
|
'praxis.charts.runtime.comparisonCurrent': 'Atual',
|
|
10466
11941
|
'praxis.charts.runtime.comparisonPrevious': 'Anterior',
|
|
10467
11942
|
'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.',
|
|
@@ -10502,6 +11977,7 @@ const PRAXIS_CHARTS_PT_BR = {
|
|
|
10502
11977
|
'praxis.charts.editor.field.comparisonMode': 'Modo de comparação',
|
|
10503
11978
|
'praxis.charts.editor.field.dimension': 'Dimensão',
|
|
10504
11979
|
'praxis.charts.editor.field.dimensionRole': 'Papel da dimensão',
|
|
11980
|
+
'praxis.charts.editor.field.dimensionExtraction': 'Extração temporal',
|
|
10505
11981
|
'praxis.charts.editor.field.metric': 'Métrica',
|
|
10506
11982
|
'praxis.charts.editor.field.metricLabel': 'Rótulo da métrica',
|
|
10507
11983
|
'praxis.charts.editor.field.metricAggregation': 'Agregação',
|
|
@@ -10655,6 +12131,8 @@ const PRAXIS_CHARTS_PT_BR = {
|
|
|
10655
12131
|
'praxis.charts.editor.specialization.scatterHint': 'Com uma métrica, o scatter usa a primeira dimensão em X e a métrica em Y. Com duas métricas, a dimensão identifica o bucket agrupado e as métricas ocupam X e Y.',
|
|
10656
12132
|
'praxis.charts.editor.specialization.gaugeTitle': 'Escala do velocímetro',
|
|
10657
12133
|
'praxis.charts.editor.specialization.gaugeHint': 'O velocímetro renderiza exatamente um bucket e uma métrica. Stats remotos usam agrupar por com limite 1; valores fora da escala falham em vez de serem limitados silenciosamente.',
|
|
12134
|
+
'praxis.charts.editor.specialization.heatmapTitle': 'Guia da matriz de calor (heatmap)',
|
|
12135
|
+
'praxis.charts.editor.specialization.heatmapHint': 'O mapa de calor renderiza uma matriz 2D exigindo duas dimensões (eixos X e Y) e uma métrica, utilizando agrupar por para stats remotos.',
|
|
10658
12136
|
'praxis.charts.editor.events.pointClickTitle': 'Clique no ponto',
|
|
10659
12137
|
'praxis.charts.editor.events.selectionChangeTitle': 'Mudança de seleção',
|
|
10660
12138
|
'praxis.charts.editor.events.drillDownTitle': 'Drill down',
|
|
@@ -10697,10 +12175,16 @@ const PRAXIS_CHARTS_PT_BR = {
|
|
|
10697
12175
|
'praxis.charts.editor.dimensionRole.segment': 'segmento',
|
|
10698
12176
|
'praxis.charts.editor.dimensionRole.time': 'tempo',
|
|
10699
12177
|
'praxis.charts.editor.dimensionRole.value': 'valor',
|
|
12178
|
+
'praxis.charts.editor.dimensionExtraction.none': 'Nenhuma',
|
|
12179
|
+
'praxis.charts.editor.dimensionExtraction.HOUR_OF_DAY': 'Hora do dia',
|
|
12180
|
+
'praxis.charts.editor.dimensionExtraction.DAY_OF_WEEK': 'Dia da semana',
|
|
12181
|
+
'praxis.charts.editor.dimensionExtraction.DAY_OF_MONTH': 'Dia do mês',
|
|
12182
|
+
'praxis.charts.editor.dimensionExtraction.MONTH_OF_YEAR': 'Mês do ano',
|
|
10700
12183
|
'praxis.charts.editor.metricAggregation.sum': 'soma',
|
|
10701
12184
|
'praxis.charts.editor.metricAggregation.count': 'contagem',
|
|
10702
12185
|
'praxis.charts.editor.metricAggregation.distinct-count': 'contagem distinta',
|
|
10703
12186
|
'praxis.charts.editor.metricAggregation.avg': 'média',
|
|
12187
|
+
'praxis.charts.editor.metricAggregation.avg-per-cycle-occurrence': 'média por ocorrência do ciclo',
|
|
10704
12188
|
'praxis.charts.editor.metricAggregation.min': 'mínimo',
|
|
10705
12189
|
'praxis.charts.editor.metricAggregation.max': 'máximo',
|
|
10706
12190
|
'praxis.charts.editor.metricAxis.primary': 'primário',
|
|
@@ -10727,6 +12211,7 @@ const PRAXIS_CHARTS_PT_BR = {
|
|
|
10727
12211
|
'praxis.charts.editor.kind.pyramid': 'Pirâmide',
|
|
10728
12212
|
'praxis.charts.editor.kind.treemap': 'Mapa de árvore',
|
|
10729
12213
|
'praxis.charts.editor.kind.gauge': 'Velocímetro',
|
|
12214
|
+
'praxis.charts.editor.kind.heatmap': 'Mapa de calor (Heatmap)',
|
|
10730
12215
|
'praxis.charts.analyticsPresentation.selectorLabel': 'Apresentação analítica',
|
|
10731
12216
|
'praxis.charts.analyticsPresentation.chart': 'Gráfico',
|
|
10732
12217
|
'praxis.charts.analyticsPresentation.table': 'Tabela',
|
|
@@ -11610,6 +13095,76 @@ const PRAXIS_CHART_BACKEND_MOCK_COMBO = {
|
|
|
11610
13095
|
},
|
|
11611
13096
|
},
|
|
11612
13097
|
};
|
|
13098
|
+
const PRAXIS_CHART_BACKEND_MOCK_HEATMAP_MATRIX = {
|
|
13099
|
+
schemaMeta: {
|
|
13100
|
+
schemaId: 'api/human-resources/vw-indicadores-incidentes|post|response|internal:false',
|
|
13101
|
+
schemaHash: 'published-hash-incident-heatmap-matrix-v1',
|
|
13102
|
+
resourcePath: 'api/human-resources/vw-indicadores-incidentes',
|
|
13103
|
+
operation: 'post',
|
|
13104
|
+
schemaType: 'response',
|
|
13105
|
+
source: 'backend',
|
|
13106
|
+
},
|
|
13107
|
+
widget: {
|
|
13108
|
+
kind: 'x-ui.chart',
|
|
13109
|
+
key: 'incident-weekday-hour-heatmap',
|
|
13110
|
+
canvasItem: { col: 1, row: 21, colSpan: 12, rowSpan: 5 },
|
|
13111
|
+
shell: {
|
|
13112
|
+
title: 'Matriz Dia da Semana x Hora',
|
|
13113
|
+
subtitle: 'Heatmap 2D com extracoes ciclicas e media por ocorrencia de ciclo',
|
|
13114
|
+
},
|
|
13115
|
+
chart: {
|
|
13116
|
+
version: '0.1.0',
|
|
13117
|
+
kind: 'heatmap',
|
|
13118
|
+
preset: 'distribution',
|
|
13119
|
+
chartId: 'incident-weekday-hour-heatmap',
|
|
13120
|
+
title: { key: 'charts.incidents.heatmap.title', fallback: 'Matriz Dia da Semana x Hora' },
|
|
13121
|
+
subtitle: {
|
|
13122
|
+
key: 'charts.incidents.heatmap.subtitle',
|
|
13123
|
+
fallback: 'Media por ocorrencia de ciclo em 7x24 via praxis.stats group-by',
|
|
13124
|
+
},
|
|
13125
|
+
sizing: { mode: 'fixed', height: 380 },
|
|
13126
|
+
source: {
|
|
13127
|
+
kind: 'praxis.stats',
|
|
13128
|
+
resource: '/api/human-resources/vw-indicadores-incidentes',
|
|
13129
|
+
operation: 'group-by',
|
|
13130
|
+
options: {
|
|
13131
|
+
timezone: 'America/Sao_Paulo',
|
|
13132
|
+
periodPreset: 'LAST_30_DAYS',
|
|
13133
|
+
fillGaps: true,
|
|
13134
|
+
orderBy: 'key-asc',
|
|
13135
|
+
limit: 168,
|
|
13136
|
+
},
|
|
13137
|
+
},
|
|
13138
|
+
dimensions: [
|
|
13139
|
+
{
|
|
13140
|
+
field: 'ocorridoEm',
|
|
13141
|
+
label: 'Dia da semana',
|
|
13142
|
+
role: 'category',
|
|
13143
|
+
extraction: 'DAY_OF_WEEK',
|
|
13144
|
+
},
|
|
13145
|
+
{
|
|
13146
|
+
field: 'ocorridoEm',
|
|
13147
|
+
label: 'Hora do dia',
|
|
13148
|
+
role: 'series',
|
|
13149
|
+
extraction: 'HOUR_OF_DAY',
|
|
13150
|
+
},
|
|
13151
|
+
],
|
|
13152
|
+
metrics: [
|
|
13153
|
+
{
|
|
13154
|
+
field: 'count',
|
|
13155
|
+
label: 'Media por ocorrencia',
|
|
13156
|
+
aggregation: 'avg-per-cycle-occurrence',
|
|
13157
|
+
format: 'decimal',
|
|
13158
|
+
},
|
|
13159
|
+
],
|
|
13160
|
+
legend: { enabled: false },
|
|
13161
|
+
tooltip: { enabled: true },
|
|
13162
|
+
theme: {
|
|
13163
|
+
palette: ['#e0f2fe', '#0284c7', '#1e3a8a'],
|
|
13164
|
+
},
|
|
13165
|
+
},
|
|
13166
|
+
},
|
|
13167
|
+
};
|
|
11613
13168
|
|
|
11614
13169
|
function buildPraxisChartMockWidgetPage(adapter) {
|
|
11615
13170
|
return {
|
|
@@ -12165,7 +13720,7 @@ class PraxisChartCompositionShowcaseComponent {
|
|
|
12165
13720
|
</article>
|
|
12166
13721
|
</div>
|
|
12167
13722
|
</section>
|
|
12168
|
-
`, isInline: true, 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"], dependencies: [{ kind: "component", type: DynamicWidgetPageComponent, selector: "praxis-dynamic-page", inputs: ["page", "context", "strictValidation", "enableCustomization", "authoringCapabilities", "showPageSettingsButton", "shellEditorComponent", "pageEditorComponent", "autoPersist", "pageIdentity", "componentInstanceId", "showWidgetAssistantButton"], outputs: ["pageChange", "persistenceChange", "pageAuthoringChange", "widgetEvent", "widgetSelectionChange", "widgetAssistantRequested", "widgetDiagnosticsChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
13723
|
+
`, isInline: true, 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"], dependencies: [{ kind: "component", type: DynamicWidgetPageComponent, selector: "praxis-dynamic-page", inputs: ["page", "context", "strictValidation", "enableCustomization", "authoringCapabilities", "showPageSettingsButton", "shellEditorComponent", "pageEditorComponent", "autoPersist", "pageIdentity", "componentInstanceId", "showWidgetAssistantButton", "blockMutations"], outputs: ["pageChange", "persistenceChange", "pageAuthoringChange", "widgetEvent", "widgetSelectionChange", "widgetAssistantRequested", "widgetDiagnosticsChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
12169
13724
|
}
|
|
12170
13725
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: PraxisChartCompositionShowcaseComponent, decorators: [{
|
|
12171
13726
|
type: Component,
|
|
@@ -12324,7 +13879,7 @@ class ChartEditorPreviewMapperService {
|
|
|
12324
13879
|
&& localData.length > 0;
|
|
12325
13880
|
const baseRows = hasLocalData
|
|
12326
13881
|
? localData.map((row) => ({ ...row }))
|
|
12327
|
-
: this.buildRows(document, domainCategories);
|
|
13882
|
+
: this.buildRows(document, domainCategories, config);
|
|
12328
13883
|
return {
|
|
12329
13884
|
config,
|
|
12330
13885
|
data: variationIndex % 2 === 1
|
|
@@ -12385,8 +13940,11 @@ class ChartEditorPreviewMapperService {
|
|
|
12385
13940
|
return nextRow;
|
|
12386
13941
|
});
|
|
12387
13942
|
}
|
|
12388
|
-
buildRows(document, domainCategories) {
|
|
12389
|
-
const
|
|
13943
|
+
buildRows(document, domainCategories, config) {
|
|
13944
|
+
const rawDimensionField = document.dimensions?.[0]?.field?.trim();
|
|
13945
|
+
const dimensionField = document.dimensions?.[0]?.extraction && config?.axes?.x?.field?.trim()
|
|
13946
|
+
? config.axes.x.field.trim()
|
|
13947
|
+
: rawDimensionField;
|
|
12390
13948
|
const metrics = document.metrics ?? [];
|
|
12391
13949
|
if (!dimensionField || !metrics.length) {
|
|
12392
13950
|
return [];
|
|
@@ -12435,6 +13993,30 @@ class ChartEditorPreviewMapperService {
|
|
|
12435
13993
|
: cleanedDomainCategories.length > 0
|
|
12436
13994
|
? cleanedDomainCategories
|
|
12437
13995
|
: ['Alpha', 'Beta', 'Gamma'];
|
|
13996
|
+
const resolvedSecondaryField = (config?.axes?.y?.type === 'category'
|
|
13997
|
+
? config.axes.y.field?.trim()
|
|
13998
|
+
: config?.series?.[0]?.secondaryCategoryField?.trim()) || document.dimensions?.[1]?.field?.trim();
|
|
13999
|
+
if (document.kind === 'heatmap') {
|
|
14000
|
+
const secondaryField = resolvedSecondaryField && resolvedSecondaryField !== dimensionField
|
|
14001
|
+
? resolvedSecondaryField
|
|
14002
|
+
: `${dimensionField}_secondary`;
|
|
14003
|
+
const primaryMetricField = metrics[0]?.field;
|
|
14004
|
+
if (!primaryMetricField) {
|
|
14005
|
+
return [];
|
|
14006
|
+
}
|
|
14007
|
+
const seriesSlots = ['08:00', '12:00', '18:00'];
|
|
14008
|
+
const rows = [];
|
|
14009
|
+
categories.slice(0, 3).forEach((category, xIdx) => {
|
|
14010
|
+
seriesSlots.forEach((slot, yIdx) => {
|
|
14011
|
+
rows.push({
|
|
14012
|
+
[dimensionField]: category,
|
|
14013
|
+
[secondaryField]: slot,
|
|
14014
|
+
[primaryMetricField]: (xIdx + 1) * 12 + yIdx * 8,
|
|
14015
|
+
});
|
|
14016
|
+
});
|
|
14017
|
+
});
|
|
14018
|
+
return rows;
|
|
14019
|
+
}
|
|
12438
14020
|
return categories.map((category, index) => {
|
|
12439
14021
|
const row = {
|
|
12440
14022
|
[dimensionField]: category,
|
|
@@ -12503,6 +14085,7 @@ class PraxisChartConfigEditor {
|
|
|
12503
14085
|
'pyramid',
|
|
12504
14086
|
'treemap',
|
|
12505
14087
|
'gauge',
|
|
14088
|
+
'heatmap',
|
|
12506
14089
|
];
|
|
12507
14090
|
sourceKinds = ['praxis.stats', 'derived'];
|
|
12508
14091
|
operations = ['group-by', 'timeseries', 'distribution', 'comparison'];
|
|
@@ -12523,7 +14106,21 @@ class PraxisChartConfigEditor {
|
|
|
12523
14106
|
'PREVIOUS_CALENDAR_PERIOD',
|
|
12524
14107
|
];
|
|
12525
14108
|
dimensionRoles = ['category', 'series', 'segment', 'time', 'value'];
|
|
12526
|
-
|
|
14109
|
+
dimensionExtractions = [
|
|
14110
|
+
'HOUR_OF_DAY',
|
|
14111
|
+
'DAY_OF_WEEK',
|
|
14112
|
+
'DAY_OF_MONTH',
|
|
14113
|
+
'MONTH_OF_YEAR',
|
|
14114
|
+
];
|
|
14115
|
+
metricAggregations = [
|
|
14116
|
+
'sum',
|
|
14117
|
+
'count',
|
|
14118
|
+
'distinct-count',
|
|
14119
|
+
'avg',
|
|
14120
|
+
'avg-per-cycle-occurrence',
|
|
14121
|
+
'min',
|
|
14122
|
+
'max',
|
|
14123
|
+
];
|
|
12527
14124
|
metricAxes = ['primary', 'secondary'];
|
|
12528
14125
|
metricSeriesKinds = ['bar', 'line', 'area'];
|
|
12529
14126
|
motionPresets = ['subtle', 'standard', 'expressive'];
|
|
@@ -12756,6 +14353,26 @@ class PraxisChartConfigEditor {
|
|
|
12756
14353
|
}
|
|
12757
14354
|
setKind(value) {
|
|
12758
14355
|
this.patchDocument((document) => {
|
|
14356
|
+
if (value === 'heatmap') {
|
|
14357
|
+
const currentDims = document.dimensions ?? [];
|
|
14358
|
+
const firstDim = currentDims[0] ?? { field: '', role: 'category' };
|
|
14359
|
+
const secondDim = currentDims[1]
|
|
14360
|
+
? { ...currentDims[1], role: 'series' }
|
|
14361
|
+
: { field: '', role: 'series' };
|
|
14362
|
+
return {
|
|
14363
|
+
...document,
|
|
14364
|
+
kind: value,
|
|
14365
|
+
gauge: undefined,
|
|
14366
|
+
dimensions: [firstDim, secondDim],
|
|
14367
|
+
metrics: document.metrics?.slice(0, 1),
|
|
14368
|
+
source: document.source.kind === 'praxis.stats'
|
|
14369
|
+
? {
|
|
14370
|
+
...document.source,
|
|
14371
|
+
operation: 'group-by',
|
|
14372
|
+
}
|
|
14373
|
+
: document.source,
|
|
14374
|
+
};
|
|
14375
|
+
}
|
|
12759
14376
|
if (value !== 'gauge') {
|
|
12760
14377
|
return {
|
|
12761
14378
|
...document,
|
|
@@ -12984,7 +14601,7 @@ class PraxisChartConfigEditor {
|
|
|
12984
14601
|
void this.resolveSelectedResource(resource, option);
|
|
12985
14602
|
}
|
|
12986
14603
|
setOperation(value) {
|
|
12987
|
-
if (this.doc().kind === 'gauge' && value !== 'group-by') {
|
|
14604
|
+
if ((this.doc().kind === 'gauge' || this.doc().kind === 'heatmap') && value !== 'group-by') {
|
|
12988
14605
|
return;
|
|
12989
14606
|
}
|
|
12990
14607
|
this.patchDocument((document) => {
|
|
@@ -13122,18 +14739,36 @@ class PraxisChartConfigEditor {
|
|
|
13122
14739
|
this.setGaugeScaleValue('max', value);
|
|
13123
14740
|
}
|
|
13124
14741
|
addDimension() {
|
|
14742
|
+
if (!this.canAddDimension()) {
|
|
14743
|
+
return;
|
|
14744
|
+
}
|
|
14745
|
+
this.patchDocument((document) => {
|
|
14746
|
+
const currentDims = document.dimensions ?? [];
|
|
14747
|
+
const nextRole = currentDims.length === 1 ? 'series' : 'category';
|
|
14748
|
+
return {
|
|
14749
|
+
...document,
|
|
14750
|
+
dimensions: [
|
|
14751
|
+
...currentDims,
|
|
14752
|
+
{ field: '', role: nextRole },
|
|
14753
|
+
],
|
|
14754
|
+
};
|
|
14755
|
+
});
|
|
14756
|
+
}
|
|
14757
|
+
removeDimension(index) {
|
|
14758
|
+
if (!this.canRemoveDimension()) {
|
|
14759
|
+
return;
|
|
14760
|
+
}
|
|
13125
14761
|
this.patchDocument((document) => ({
|
|
13126
14762
|
...document,
|
|
13127
|
-
dimensions: [
|
|
13128
|
-
...(document.dimensions ?? []),
|
|
13129
|
-
{ field: '', role: 'category' },
|
|
13130
|
-
],
|
|
14763
|
+
dimensions: (document.dimensions ?? []).filter((_, currentIndex) => currentIndex !== index),
|
|
13131
14764
|
}));
|
|
13132
14765
|
}
|
|
13133
|
-
|
|
14766
|
+
setDimensionExtraction(index, extraction) {
|
|
13134
14767
|
this.patchDocument((document) => ({
|
|
13135
14768
|
...document,
|
|
13136
|
-
dimensions: (document.dimensions ?? []).
|
|
14769
|
+
dimensions: (document.dimensions ?? []).map((dimension, currentIndex) => currentIndex === index
|
|
14770
|
+
? { ...dimension, extraction: extraction || undefined }
|
|
14771
|
+
: dimension),
|
|
13137
14772
|
}));
|
|
13138
14773
|
}
|
|
13139
14774
|
setDimensionField(index, field) {
|
|
@@ -13223,12 +14858,31 @@ class PraxisChartConfigEditor {
|
|
|
13223
14858
|
}));
|
|
13224
14859
|
}
|
|
13225
14860
|
setMetricAggregation(index, aggregation) {
|
|
13226
|
-
this.patchDocument((document) =>
|
|
13227
|
-
|
|
13228
|
-
metrics: (document.metrics ?? []).map((metric, currentIndex) => currentIndex === index
|
|
14861
|
+
this.patchDocument((document) => {
|
|
14862
|
+
const nextMetrics = (document.metrics ?? []).map((metric, currentIndex) => currentIndex === index
|
|
13229
14863
|
? { ...metric, aggregation }
|
|
13230
|
-
: metric)
|
|
13231
|
-
|
|
14864
|
+
: metric);
|
|
14865
|
+
if (aggregation === 'avg-per-cycle-occurrence' && document.source.kind === 'praxis.stats') {
|
|
14866
|
+
const currentOpts = document.source.options ?? {};
|
|
14867
|
+
const hasPeriod = Boolean(currentOpts.periodPreset);
|
|
14868
|
+
return {
|
|
14869
|
+
...document,
|
|
14870
|
+
metrics: nextMetrics,
|
|
14871
|
+
source: {
|
|
14872
|
+
...document.source,
|
|
14873
|
+
options: {
|
|
14874
|
+
...currentOpts,
|
|
14875
|
+
timezone: currentOpts.timezone || 'UTC',
|
|
14876
|
+
...(hasPeriod ? {} : { periodPreset: 'LAST_30_DAYS' }),
|
|
14877
|
+
},
|
|
14878
|
+
},
|
|
14879
|
+
};
|
|
14880
|
+
}
|
|
14881
|
+
return {
|
|
14882
|
+
...document,
|
|
14883
|
+
metrics: nextMetrics,
|
|
14884
|
+
};
|
|
14885
|
+
});
|
|
13232
14886
|
}
|
|
13233
14887
|
setMetricAxis(index, axis) {
|
|
13234
14888
|
this.patchDocument((document) => ({
|
|
@@ -13450,7 +15104,7 @@ class PraxisChartConfigEditor {
|
|
|
13450
15104
|
const availableOperations = !selectedResource
|
|
13451
15105
|
? this.resourceOptions().length ? this.operations : []
|
|
13452
15106
|
: selectedResource.operations?.length ? selectedResource.operations : [];
|
|
13453
|
-
if (this.doc().kind === 'gauge') {
|
|
15107
|
+
if (this.doc().kind === 'gauge' || this.doc().kind === 'heatmap') {
|
|
13454
15108
|
return availableOperations.filter((operation) => operation === 'group-by');
|
|
13455
15109
|
}
|
|
13456
15110
|
return availableOperations;
|
|
@@ -13618,8 +15272,28 @@ class PraxisChartConfigEditor {
|
|
|
13618
15272
|
metrics() {
|
|
13619
15273
|
return this.doc().metrics ?? [];
|
|
13620
15274
|
}
|
|
13621
|
-
|
|
15275
|
+
canAddDimension() {
|
|
15276
|
+
const count = this.dimensions().length;
|
|
13622
15277
|
if (this.doc().kind === 'gauge') {
|
|
15278
|
+
return count < 1;
|
|
15279
|
+
}
|
|
15280
|
+
if (this.doc().kind === 'heatmap') {
|
|
15281
|
+
return count < 2;
|
|
15282
|
+
}
|
|
15283
|
+
if (this.doc().source.kind === 'praxis.stats' && this.doc().source.operation === 'group-by') {
|
|
15284
|
+
return count < 2;
|
|
15285
|
+
}
|
|
15286
|
+
return true;
|
|
15287
|
+
}
|
|
15288
|
+
canRemoveDimension() {
|
|
15289
|
+
const count = this.dimensions().length;
|
|
15290
|
+
if (this.doc().kind === 'heatmap') {
|
|
15291
|
+
return count > 2;
|
|
15292
|
+
}
|
|
15293
|
+
return count > 1;
|
|
15294
|
+
}
|
|
15295
|
+
canAddMetric() {
|
|
15296
|
+
if (this.doc().kind === 'gauge' || this.doc().kind === 'heatmap') {
|
|
13623
15297
|
return false;
|
|
13624
15298
|
}
|
|
13625
15299
|
return this.doc().kind !== 'scatter' || this.metrics().length < 2;
|
|
@@ -13666,9 +15340,18 @@ class PraxisChartConfigEditor {
|
|
|
13666
15340
|
metricAggregationOptions(fieldName) {
|
|
13667
15341
|
const field = this.fieldOptions('metric').find((option) => option.field === fieldName) ??
|
|
13668
15342
|
this.availableFields().find((option) => option.field === fieldName);
|
|
13669
|
-
const
|
|
15343
|
+
const hasTemporalExtraction = (this.doc().dimensions ?? []).some((dim) => Boolean(dim.extraction));
|
|
15344
|
+
const currentUsesCycleOccurrence = (this.doc().metrics ?? []).some((metric) => metric.aggregation === 'avg-per-cycle-occurrence');
|
|
15345
|
+
const baseAggregations = field?.aggregations?.length
|
|
13670
15346
|
? field.aggregations.filter((aggregation) => this.metricAggregations.includes(aggregation))
|
|
13671
|
-
: this.metricAggregations
|
|
15347
|
+
: this.metricAggregations.filter((aggregation) => aggregation !== 'avg-per-cycle-occurrence'
|
|
15348
|
+
|| hasTemporalExtraction
|
|
15349
|
+
|| currentUsesCycleOccurrence);
|
|
15350
|
+
const availableAggregations = (hasTemporalExtraction || currentUsesCycleOccurrence)
|
|
15351
|
+
&& !baseAggregations.includes('avg-per-cycle-occurrence')
|
|
15352
|
+
&& (this.doc().source.kind !== 'praxis.stats' || this.doc().source.operation === 'group-by')
|
|
15353
|
+
? [...baseAggregations, 'avg-per-cycle-occurrence']
|
|
15354
|
+
: baseAggregations;
|
|
13672
15355
|
return this.doc().source.kind === 'praxis.stats' && this.doc().source.operation === 'comparison'
|
|
13673
15356
|
? availableAggregations.filter((aggregation) => ['count', 'distinct-count', 'sum'].includes(aggregation))
|
|
13674
15357
|
: availableAggregations;
|
|
@@ -13710,6 +15393,9 @@ class PraxisChartConfigEditor {
|
|
|
13710
15393
|
showGaugePanel() {
|
|
13711
15394
|
return this.doc().kind === 'gauge';
|
|
13712
15395
|
}
|
|
15396
|
+
showHeatmapPanel() {
|
|
15397
|
+
return this.doc().kind === 'heatmap';
|
|
15398
|
+
}
|
|
13713
15399
|
gaugeMinValue() {
|
|
13714
15400
|
const min = this.doc().gauge?.scale.min;
|
|
13715
15401
|
return min === undefined ? '' : String(min);
|
|
@@ -13922,7 +15608,9 @@ class PraxisChartConfigEditor {
|
|
|
13922
15608
|
const isTemporal = doc.kind === 'line' ||
|
|
13923
15609
|
doc.kind === 'area' ||
|
|
13924
15610
|
(doc.source.kind === 'praxis.stats' && doc.source.operation === 'timeseries');
|
|
13925
|
-
const isStacked = doc.kind === 'stacked-bar' ||
|
|
15611
|
+
const isStacked = doc.kind === 'stacked-bar' ||
|
|
15612
|
+
doc.kind === 'stacked-area' ||
|
|
15613
|
+
doc.kind === 'heatmap';
|
|
13926
15614
|
const tokenize = (raw) => String(raw || '')
|
|
13927
15615
|
.trim()
|
|
13928
15616
|
.normalize('NFD')
|
|
@@ -14080,7 +15768,9 @@ class PraxisChartConfigEditor {
|
|
|
14080
15768
|
(!currentDimField ||
|
|
14081
15769
|
currentDimField === 'category' ||
|
|
14082
15770
|
(validDimSet.size > 0 && !validDimSet.has(currentDimField)));
|
|
14083
|
-
const isStacked = this.doc().kind === 'stacked-bar' ||
|
|
15771
|
+
const isStacked = this.doc().kind === 'stacked-bar' ||
|
|
15772
|
+
this.doc().kind === 'stacked-area' ||
|
|
15773
|
+
this.doc().kind === 'heatmap';
|
|
14084
15774
|
const seriesNeedsCalibration = isStacked &&
|
|
14085
15775
|
Boolean(rec.seriesField) &&
|
|
14086
15776
|
(!currentSeriesField ||
|
|
@@ -14226,12 +15916,18 @@ class PraxisChartConfigEditor {
|
|
|
14226
15916
|
&& Number.isFinite(scaleMin)
|
|
14227
15917
|
&& Number.isFinite(scaleMax)
|
|
14228
15918
|
&& scaleMax > scaleMin;
|
|
15919
|
+
const fallbackDimensions = base.kind === 'heatmap'
|
|
15920
|
+
? [
|
|
15921
|
+
validDimensions[0] ?? { field: 'category', role: 'category' },
|
|
15922
|
+
validDimensions[1] ?? { field: 'series', role: 'series' },
|
|
15923
|
+
]
|
|
15924
|
+
: validDimensions.length
|
|
15925
|
+
? validDimensions
|
|
15926
|
+
: [{ field: 'category', role: 'category' }];
|
|
14229
15927
|
const syntheticContract = {
|
|
14230
15928
|
...structuredClone(base),
|
|
14231
15929
|
source: { kind: 'derived' },
|
|
14232
|
-
dimensions:
|
|
14233
|
-
? validDimensions
|
|
14234
|
-
: [{ field: 'category', role: 'category' }],
|
|
15930
|
+
dimensions: fallbackDimensions,
|
|
14235
15931
|
metrics: fallbackMetrics,
|
|
14236
15932
|
gauge: base.kind === 'gauge'
|
|
14237
15933
|
? {
|
|
@@ -14909,7 +16605,7 @@ class PraxisChartConfigEditor {
|
|
|
14909
16605
|
}
|
|
14910
16606
|
}
|
|
14911
16607
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: PraxisChartConfigEditor, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
14912
|
-
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 }, localDataInput: { classPropertyName: "localDataInput", publicName: "localData", 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", localDataChange: "localDataChange" }, providers: [providePraxisChartsI18n()], ngImport: i0, template: "<div class=\"editor-shell\" [class.editor-shell--preview-active]=\"activeSection() === 'preview'\">\n <div class=\"editor-nav\" role=\"toolbar\" [attr.aria-label]=\"t('praxis.charts.editor.sectionsNav', 'Se\u00E7\u00F5es do editor')\">\n @for (section of sections; track section.id) {\n <button\n mat-stroked-button\n type=\"button\"\n [attr.id]=\"'chart-editor-tab-' + section.id\"\n aria-controls=\"chart-editor-tabpanel\"\n [attr.aria-pressed]=\"activeSection() === section.id\"\n [tabIndex]=\"activeSection() === section.id ? 0 : -1\"\n [attr.data-testid]=\"'chart-editor-section-' + section.id\"\n [class.active]=\"activeSection() === section.id\"\n [class.editor-nav__preview-btn]=\"section.id === 'preview'\"\n [class.editor-nav__has-error]=\"sectionHasError(section.id)\"\n (click)=\"setSection(section.id)\"\n (keydown)=\"onSectionNavKeydown($event, $index)\"\n >\n <span>{{ t(section.labelKey, section.fallback) }}</span>\n @if (sectionIssueCount(section.id) > 0) {\n <span\n class=\"editor-nav__badge\"\n aria-hidden=\"true\"\n [class.editor-nav__badge--error]=\"sectionHasError(section.id)\"\n [attr.data-testid]=\"'chart-editor-section-badge-' + section.id\"\n >\n {{ sectionIssueCount(section.id) }}\n </span>\n }\n </button>\n }\n </div>\n\n <div class=\"editor-layout\">\n <div\n class=\"editor-form\"\n id=\"chart-editor-tabpanel\"\n role=\"tabpanel\"\n [attr.aria-labelledby]=\"'chart-editor-tab-' + activeSection()\"\n >\n @switch (activeSection()) {\n @case ('general') {\n <mat-card class=\"editor-card\">\n <mat-card-content 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 </mat-card-content>\n </mat-card>\n }\n\n @case ('data') {\n <div class=\"editor-stack\">\n <mat-card class=\"editor-card\">\n <mat-card-content class=\"editor-stack\">\n <p class=\"editor-caption\" data-testid=\"chart-editor-effective-origin\">\n {{ effectiveDataOriginSummary() }}\n </p>\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\n data-testid=\"chart-editor-resource\"\n [ngModel]=\"resourceValue()\"\n (ngModelChange)=\"setResource($event)\"\n (openedChange)=\"setResourceSearchTerm('')\"\n [disabled]=\"isReadonly() || resourceCatalogUnavailable()\"\n >\n @if (selectedResourceOption(); as selectedRes) {\n <mat-select-trigger>\n <span class=\"editor-select-trigger\">\n <mat-icon class=\"editor-select-trigger__icon\" [praxisIcon]=\"selectedRes.icon || 'api'\"></mat-icon>\n <span class=\"editor-select-trigger__label\">{{ selectedRes.label }}</span>\n <span class=\"editor-select-trigger__code\">{{ selectedRes.path }}</span>\n </span>\n </mat-select-trigger>\n }\n <div class=\"editor-select-search\" (click)=\"$event.stopPropagation()\">\n <mat-icon aria-hidden=\"true\">search</mat-icon>\n <input\n type=\"search\"\n data-testid=\"chart-editor-resource-search\"\n [placeholder]=\"t('praxis.charts.editor.field.resourceSearchPlaceholder', 'Search by name, key, or endpoint...')\"\n [ngModel]=\"resourceSearchTerm()\"\n (ngModelChange)=\"setResourceSearchTerm($event)\"\n (keydown)=\"onSelectSearchKeydown($event)\"\n />\n </div>\n @for (resource of filteredResourceOptions(); track resource.id) {\n <mat-option [value]=\"resource.path\" class=\"editor-rich-mat-option\">\n <div class=\"editor-rich-option\">\n <span class=\"editor-rich-option__icon\" aria-hidden=\"true\">\n <mat-icon [praxisIcon]=\"resource.icon || 'api'\"></mat-icon>\n </span>\n <div class=\"editor-rich-option__body\">\n <div class=\"editor-rich-option__header\">\n <span class=\"editor-rich-option__label\">{{ resource.label }}</span>\n @if (resource.group) {\n <span class=\"editor-rich-option__badge\">{{ resource.group }}</span>\n }\n @if (resource.readOnly) {\n <span class=\"editor-rich-option__badge\">{{ t('praxis.charts.editor.catalog.readOnly', 'Read-only') }}</span>\n }\n </div>\n <span class=\"editor-rich-option__code\">\n {{ resource.path }}{{ resource.id && resource.id !== resource.path && resource.id !== resource.label ? ' \u00B7 ' + resource.id : '' }}\n </span>\n @if (resource.description) {\n <span class=\"editor-rich-option__description\">{{ resource.description }}</span>\n }\n </div>\n </div>\n </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() || isResolvingResource() || 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 (isResolvingResource()) {\n <mat-hint data-testid=\"chart-editor-resolving-resource\">\n {{ t('praxis.charts.editor.catalog.resolvingResource', 'Resolving governed stats operations and fields for the selected resource...') }}\n </mat-hint>\n } @else 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 (doc().source.kind === 'praxis.stats' && selectedResourceOption(); as selectedRes) {\n <div class=\"editor-resource-summary\" data-testid=\"chart-editor-resource-summary\">\n <span class=\"editor-resource-summary__icon\" aria-hidden=\"true\">\n <mat-icon [praxisIcon]=\"selectedRes.icon || 'api'\"></mat-icon>\n </span>\n <div class=\"editor-resource-summary__body\">\n <div class=\"editor-resource-summary__header\">\n <strong>{{ selectedRes.label }}</strong>\n @if (selectedRes.group) {\n <span class=\"editor-resource-summary__badge\">{{ selectedRes.group }}</span>\n }\n @if (selectedRes.readOnly) {\n <span class=\"editor-resource-summary__badge\">{{ t('praxis.charts.editor.catalog.readOnly', 'Read-only') }}</span>\n }\n </div>\n <span class=\"editor-resource-summary__code\">\n {{ selectedRes.path }}{{ selectedRes.id && selectedRes.id !== selectedRes.path && selectedRes.id !== selectedRes.label ? ' \u00B7 ' + selectedRes.id : '' }}\n </span>\n @if (selectedRes.description) {\n <p class=\"editor-resource-summary__description\">{{ selectedRes.description }}</p>\n }\n </div>\n </div>\n }\n </mat-card-content>\n </mat-card>\n\n @if (showTimeseriesControls()) {\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.specialization.timeseriesTitle', 'Timeseries options') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.granularity', 'Granularity') }}</mat-label>\n <mat-select [ngModel]=\"granularityValue()\" (ngModelChange)=\"setGranularity($event)\" [disabled]=\"isReadonly()\">\n @for (granularity of timeGranularities; track granularity) {\n <mat-option [value]=\"granularity\">\n {{ t('praxis.charts.editor.granularity.' + granularity, granularity) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n <mat-slide-toggle\n [ngModel]=\"fillGapsValue()\"\n (ngModelChange)=\"setFillGaps($event)\"\n [disabled]=\"isReadonly()\"\n >\n {{ t('praxis.charts.editor.field.fillGaps', 'Fill missing intervals') }}\n </mat-slide-toggle>\n </mat-card-content>\n </mat-card>\n }\n\n @if (showDistributionControls()) {\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.specialization.distributionTitle', 'Distribution options') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.distributionMode', 'Distribution mode') }}</mat-label>\n <mat-select data-testid=\"chart-editor-distribution-mode\" [ngModel]=\"distributionModeValue()\" (ngModelChange)=\"setDistributionMode($event)\" [disabled]=\"isReadonly()\">\n @for (mode of distributionModes; track mode) {\n <mat-option [value]=\"mode\">\n {{ t('praxis.charts.editor.distributionMode.' + mode, mode) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n @if (distributionModeValue() === 'histogram') {\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.bucketSize', 'Bucket size') }}</mat-label>\n <input matInput data-testid=\"chart-editor-bucket-size\" [ngModel]=\"bucketSizeValue()\" (ngModelChange)=\"setBucketSize($event)\" [disabled]=\"isReadonly()\" />\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.bucketCount', 'Bucket count') }}</mat-label>\n <input matInput data-testid=\"chart-editor-bucket-count\" [ngModel]=\"bucketCountValue()\" (ngModelChange)=\"setBucketCount($event)\" [disabled]=\"isReadonly()\" />\n </mat-form-field>\n }\n </mat-card-content>\n </mat-card>\n }\n\n @if (showComparisonControls()) {\n <mat-card class=\"editor-card\" data-testid=\"chart-editor-comparison-options\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.specialization.comparisonTitle', 'Comparison period') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.comparisonPeriodField', 'Period field') }}</mat-label>\n @if (fieldOptions('time').length) {\n <mat-select\n [ngModel]=\"comparisonPeriodFieldValue()\"\n (ngModelChange)=\"setComparisonPeriodField($event)\"\n (openedChange)=\"setFieldSearchTerm('')\"\n [disabled]=\"isReadonly()\"\n >\n <div class=\"editor-select-search\" (click)=\"$event.stopPropagation()\">\n <mat-icon aria-hidden=\"true\">search</mat-icon>\n <input\n type=\"search\"\n [placeholder]=\"t('praxis.charts.editor.field.fieldSearchPlaceholder', 'Search field...')\"\n [ngModel]=\"fieldSearchTerm()\"\n (ngModelChange)=\"setFieldSearchTerm($event)\"\n (keydown)=\"onSelectSearchKeydown($event)\"\n />\n </div>\n @for (field of filteredFieldOptions('time', comparisonPeriodFieldValue()); track field.field) {\n <mat-option [value]=\"field.field\" class=\"editor-rich-mat-option\">\n <div class=\"editor-rich-option\">\n <div class=\"editor-rich-option__body\">\n <div class=\"editor-rich-option__header\">\n <span class=\"editor-rich-option__label\">{{ field.label || field.field }}</span>\n @if (field.type) {\n <span class=\"editor-rich-option__badge\">{{ field.type }}</span>\n }\n </div>\n @if (field.label && field.label !== field.field) {\n <span class=\"editor-rich-option__code\">{{ field.field }}</span>\n }\n @if (field.description) {\n <span class=\"editor-rich-option__description\">{{ field.description }}</span>\n }\n </div>\n </div>\n </mat-option>\n }\n </mat-select>\n } @else {\n <mat-select [ngModel]=\"comparisonPeriodFieldValue()\" [disabled]=\"true\"></mat-select>\n }\n @if (!fieldOptions('time').length) {\n <mat-hint>{{ t('praxis.charts.editor.catalog.comparisonPeriodFieldsMissing', 'No governed time-series field is available for comparison periods.') }}</mat-hint>\n }\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.comparisonTimezone', 'Timezone') }}</mat-label>\n <input\n matInput\n [ngModel]=\"comparisonPeriodTimezoneValue()\"\n (ngModelChange)=\"setComparisonPeriodTimezone($event)\"\n [disabled]=\"isReadonly()\"\n />\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.comparisonPreset', 'Period preset') }}</mat-label>\n <mat-select\n [ngModel]=\"comparisonPeriodPresetValue()\"\n (ngModelChange)=\"setComparisonPeriodPreset($event)\"\n [disabled]=\"isReadonly()\"\n >\n @for (preset of comparisonPeriodPresets; track preset) {\n <mat-option [value]=\"preset\">\n {{ t('praxis.charts.editor.comparisonPreset.' + preset, preset) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.comparisonMode', 'Comparison mode') }}</mat-label>\n <mat-select\n [ngModel]=\"comparisonPeriodModeValue()\"\n (ngModelChange)=\"setComparisonPeriodMode($event)\"\n [disabled]=\"isReadonly()\"\n >\n @for (mode of comparisonPeriodModes; track mode) {\n <mat-option [value]=\"mode\">\n {{ t('praxis.charts.editor.comparisonMode.' + mode, mode) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n </mat-card-content>\n </mat-card>\n }\n\n @if (doc().source.kind === 'derived') {\n <mat-card class=\"editor-card\" data-testid=\"chart-editor-local-data\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.localData.title', 'Local dataset (inputs.data)') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-stack\">\n <p class=\"editor-caption\">\n {{ t('praxis.charts.editor.localData.caption', 'Edit the local records rendered when the data source is derived.') }}\n </p>\n\n @if (canConsolidateGaugeLocalData()) {\n <div class=\"editor-row-actions\">\n <button\n mat-stroked-button\n type=\"button\"\n data-testid=\"chart-local-data-consolidate-gauge\"\n (click)=\"consolidateGaugeLocalData()\"\n [disabled]=\"isReadonly()\"\n >\n {{ t('praxis.charts.editor.localData.consolidateGauge', 'Consolidate to 1 bucket for gauge') }}\n </button>\n </div>\n }\n\n @if (localDataRows().length) {\n @for (row of localDataRows(); track $index; let rowIndex = $index) {\n <div class=\"editor-row-card\" [attr.data-testid]=\"'chart-local-data-row-' + rowIndex\">\n <div class=\"editor-grid\">\n @for (dimField of localDataDimensionFields(); track dimField) {\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ dimField }}</mat-label>\n <input\n matInput\n [attr.data-testid]=\"'chart-local-data-dim-' + rowIndex + '-' + dimField\"\n [ngModel]=\"localDataCellStringValue(rowIndex, dimField)\"\n (ngModelChange)=\"setLocalDataCellValue(rowIndex, dimField, $event, 'dimension')\"\n [disabled]=\"isReadonly()\"\n />\n </mat-form-field>\n }\n @for (metricField of localDataMetricFields(); track metricField) {\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ metricField }}</mat-label>\n <input\n matInput\n type=\"number\"\n [attr.data-testid]=\"'chart-local-data-metric-' + rowIndex + '-' + metricField\"\n [ngModel]=\"localDataCellStringValue(rowIndex, metricField)\"\n (ngModelChange)=\"setLocalDataCellValue(rowIndex, metricField, $event, 'metric')\"\n [disabled]=\"isReadonly()\"\n />\n </mat-form-field>\n }\n </div>\n <div class=\"editor-row-actions\">\n <button\n mat-button\n type=\"button\"\n [attr.data-testid]=\"'chart-local-data-remove-row-' + rowIndex\"\n (click)=\"removeLocalDataRow(rowIndex)\"\n [disabled]=\"isReadonly()\"\n >\n {{ t('praxis.charts.editor.localData.removeRow', 'Remove record') }}\n </button>\n </div>\n </div>\n }\n } @else {\n <div class=\"editor-empty\" data-testid=\"chart-local-data-empty\">\n {{ t('praxis.charts.editor.localData.empty', 'No local records configured yet. Add a record to replace the synthetic preview sample.') }}\n </div>\n }\n\n <div>\n <button\n mat-stroked-button\n type=\"button\"\n data-testid=\"chart-local-data-add-row\"\n (click)=\"addLocalDataRow()\"\n [disabled]=\"isReadonly()\"\n >\n {{ t('praxis.charts.editor.localData.addRow', 'Add record') }}\n </button>\n </div>\n </mat-card-content>\n </mat-card>\n }\n </div>\n }\n\n @case ('motion') {\n <mat-card class=\"editor-card\">\n <mat-card-content class=\"editor-stack\">\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 <p class=\"editor-caption\" data-testid=\"chart-editor-motion-hint\">\n {{ t('praxis.charts.editor.motion.hint', 'Controls initial entrance timing and smooth transitions when filters or query context change, with automatic volume thresholds for high-density datasets.') }}\n </p>\n\n <div>\n <button\n mat-stroked-button\n type=\"button\"\n data-testid=\"chart-editor-simulate-motion\"\n (click)=\"simulateMotionTransition()\"\n [disabled]=\"normalizedDocument().motion?.enabled === false || !effectivePreview()\"\n >\n {{ t('praxis.charts.editor.motion.simulate', 'Simulate transition') }}\n </button>\n </div>\n </mat-card-content>\n </mat-card>\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-toggle-cluster\">\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 <div class=\"editor-grid\">\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 </div>\n\n @if (canApplyDomainSemanticPalette()) {\n <div class=\"editor-row-actions\">\n <button\n mat-stroked-button\n type=\"button\"\n data-testid=\"chart-editor-apply-domain-palette\"\n (click)=\"applyDomainSemanticPalette()\"\n [disabled]=\"isReadonly()\"\n [title]=\"t('praxis.charts.editor.appearance.applyDomainPaletteHint', 'Generates a custom palette with semantic colors (success, warning, danger, info) from the dimension categories.')\"\n >\n <mat-icon aria-hidden=\"true\">auto_fix_high</mat-icon>\n {{ t('praxis.charts.editor.appearance.applyDomainPalette', 'Apply domain semantic colors') }}\n </button>\n </div>\n }\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\n <div class=\"editor-palette-swatches\" data-testid=\"chart-editor-palette-swatches\">\n @for (swatch of tokenPaletteSwatches(); track $index) {\n <span class=\"editor-palette-swatch-chip\" [title]=\"swatch\">\n <span class=\"editor-palette-swatch-dot\" [style.background]=\"swatch\"></span>\n <code class=\"editor-palette-swatch-code\">{{ swatch }}</code>\n </span>\n }\n </div>\n } @else {\n <div class=\"editor-palette-swatches\" data-testid=\"chart-editor-palette-swatches\">\n @for (swatch of customPaletteColors(); track $index) {\n <span class=\"editor-palette-swatch-chip\" [title]=\"swatch\">\n <span class=\"editor-palette-swatch-dot\" [style.background]=\"swatch\"></span>\n <input\n type=\"color\"\n class=\"editor-palette-color-input\"\n [attr.data-testid]=\"'chart-editor-palette-color-' + $index\"\n [value]=\"swatchHexValue(swatch, $index)\"\n (input)=\"setCustomPaletteColorAt($index, $any($event.target).value)\"\n [disabled]=\"isReadonly()\"\n />\n <code class=\"editor-palette-swatch-code\">{{ swatch }}</code>\n <button\n type=\"button\"\n class=\"editor-palette-swatch-remove\"\n [attr.data-testid]=\"'chart-editor-palette-remove-' + $index\"\n (click)=\"removeCustomPaletteColor($index)\"\n [disabled]=\"isReadonly()\"\n [attr.aria-label]=\"t('praxis.charts.editor.appearance.removePaletteColor', 'Remove color')\"\n >\n \u00D7\n </button>\n </span>\n }\n <button\n mat-stroked-button\n type=\"button\"\n class=\"editor-palette-add-btn\"\n data-testid=\"chart-editor-add-palette-color\"\n (click)=\"addCustomPaletteColor()\"\n [disabled]=\"isReadonly()\"\n >\n + {{ t('praxis.charts.editor.appearance.addPaletteColor', 'Add color') }}\n </button>\n </div>\n\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=\"2\"\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-state-group\">\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=\"1\" [ngModel]=\"stateDescription('empty')\" (ngModelChange)=\"setStateDescription('empty', $event)\" [disabled]=\"isReadonly()\"></textarea>\n </mat-form-field>\n </div>\n </div>\n\n <div class=\"editor-state-group\">\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=\"1\" [ngModel]=\"stateDescription('loading')\" (ngModelChange)=\"setStateDescription('loading', $event)\" [disabled]=\"isReadonly()\"></textarea>\n </mat-form-field>\n </div>\n </div>\n\n <div class=\"editor-state-group\">\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=\"1\" [ngModel]=\"stateDescription('error')\" (ngModelChange)=\"setStateDescription('error', $event)\" [disabled]=\"isReadonly()\"></textarea>\n </mat-form-field>\n </div>\n </div>\n </mat-card-content>\n </mat-card>\n </div>\n }\n\n @case ('analytics') {\n <div class=\"editor-stack\">\n @if (showGaugePanel()) {\n <mat-card class=\"editor-card\" data-testid=\"chart-editor-gauge-scale\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.specialization.gaugeTitle', 'Gauge scale') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-stack\">\n <p class=\"editor-caption\">\n {{ t('praxis.charts.editor.specialization.gaugeHint', 'Gauge charts render exactly one bucket and one metric. Remote stats use group-by with limit 1; values outside the scale fail instead of being clamped.') }}\n </p>\n <div class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.gaugeMin', 'Scale minimum') }}</mat-label>\n <input\n matInput\n type=\"number\"\n data-testid=\"chart-editor-gauge-min\"\n [ngModel]=\"gaugeMinValue()\"\n (ngModelChange)=\"setGaugeMin($event)\"\n [disabled]=\"isReadonly()\"\n />\n </mat-form-field>\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.gaugeMax', 'Scale maximum') }}</mat-label>\n <input\n matInput\n type=\"number\"\n data-testid=\"chart-editor-gauge-max\"\n [ngModel]=\"gaugeMaxValue()\"\n (ngModelChange)=\"setGaugeMax($event)\"\n [disabled]=\"isReadonly()\"\n />\n </mat-form-field>\n </div>\n </mat-card-content>\n </mat-card>\n }\n\n @if (showComboPanel()) {\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.specialization.comboTitle', 'Combo guidance') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content>\n <p class=\"editor-caption\">\n {{ t('praxis.charts.editor.specialization.comboHint', 'Combo charts require at least two metrics and allow per-metric axis and series kind mapping.') }}\n </p>\n </mat-card-content>\n </mat-card>\n }\n\n @if (showPieDonutPanel()) {\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.specialization.pieDonutTitle', 'Composition guidance') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content>\n <p class=\"editor-caption\">\n {{ t('praxis.charts.editor.specialization.pieDonutHint', 'Pie, donut, funnel, pyramid and treemap charts keep one metric and use the first dimension as the category segment.') }}\n </p>\n </mat-card-content>\n </mat-card>\n }\n\n @if (showScatterPanel()) {\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.specialization.scatterTitle', 'Scatter guidance') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content>\n <p class=\"editor-caption\">\n {{ t('praxis.charts.editor.specialization.scatterHint', 'With one metric, scatter uses the first dimension as X and the metric as Y. With two metrics, the dimension identifies the grouped bucket and the metrics map to X and Y.') }}\n </p>\n </mat-card-content>\n </mat-card>\n }\n\n @if (canApplySemanticAnalyticsMapping()) {\n <div class=\"editor-row-actions\">\n <button\n mat-stroked-button\n type=\"button\"\n data-testid=\"chart-editor-smart-map-analytics\"\n (click)=\"applySemanticAnalyticsMapping()\"\n [disabled]=\"isReadonly()\"\n [title]=\"t('praxis.charts.editor.analytics.smartMapHint', 'Automatically selects the best dimension, metric, and aggregation from the governed field catalog.')\"\n >\n <mat-icon aria-hidden=\"true\">auto_fix_high</mat-icon>\n {{ t('praxis.charts.editor.analytics.smartMap', 'Suggest dimension & metric from schema') }}\n </button>\n </div>\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\n [ngModel]=\"dimension.field || ''\"\n (ngModelChange)=\"setDimensionField($index, $event)\"\n (openedChange)=\"setFieldSearchTerm('')\"\n [disabled]=\"isReadonly()\"\n >\n <div class=\"editor-select-search\" (click)=\"$event.stopPropagation()\">\n <mat-icon aria-hidden=\"true\">search</mat-icon>\n <input\n type=\"search\"\n [placeholder]=\"t('praxis.charts.editor.field.fieldSearchPlaceholder', 'Search field...')\"\n [ngModel]=\"fieldSearchTerm()\"\n (ngModelChange)=\"setFieldSearchTerm($event)\"\n (keydown)=\"onSelectSearchKeydown($event)\"\n />\n </div>\n @for (field of filteredFieldOptions('dimension', dimension.field); track field.field) {\n <mat-option [value]=\"field.field\" class=\"editor-rich-mat-option\">\n <div class=\"editor-rich-option\">\n <div class=\"editor-rich-option__body\">\n <div class=\"editor-rich-option__header\">\n <span class=\"editor-rich-option__label\">{{ field.label || field.field }}</span>\n @if (field.type) {\n <span class=\"editor-rich-option__badge\">{{ field.type }}</span>\n }\n </div>\n @if (field.label && field.label !== field.field) {\n <span class=\"editor-rich-option__code\">{{ field.field }}</span>\n }\n @if (field.description) {\n <span class=\"editor-rich-option__description\">{{ field.description }}</span>\n }\n </div>\n </div>\n </mat-option>\n }\n </mat-select>\n } @else {\n <mat-select [ngModel]=\"dimension.field || ''\" (ngModelChange)=\"setDimensionField($index, $event)\" [disabled]=\"true\"></mat-select>\n }\n @if (!fieldOptions('dimension').length) {\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\n [ngModel]=\"metric.field || ''\"\n (ngModelChange)=\"setMetricField($index, $event)\"\n (openedChange)=\"setFieldSearchTerm('')\"\n [disabled]=\"isReadonly()\"\n >\n <div class=\"editor-select-search\" (click)=\"$event.stopPropagation()\">\n <mat-icon aria-hidden=\"true\">search</mat-icon>\n <input\n type=\"search\"\n [placeholder]=\"t('praxis.charts.editor.field.fieldSearchPlaceholder', 'Search field...')\"\n [ngModel]=\"fieldSearchTerm()\"\n (ngModelChange)=\"setFieldSearchTerm($event)\"\n (keydown)=\"onSelectSearchKeydown($event)\"\n />\n </div>\n @for (field of filteredFieldOptions('metric', metric.field); track field.field) {\n <mat-option [value]=\"field.field\" class=\"editor-rich-mat-option\">\n <div class=\"editor-rich-option\">\n <div class=\"editor-rich-option__body\">\n <div class=\"editor-rich-option__header\">\n <span class=\"editor-rich-option__label\">{{ field.label || field.field }}</span>\n @if (field.type) {\n <span class=\"editor-rich-option__badge\">{{ field.type }}</span>\n }\n </div>\n @if (field.label && field.label !== field.field) {\n <span class=\"editor-rich-option__code\">{{ field.field }}</span>\n }\n @if (field.description) {\n <span class=\"editor-rich-option__description\">{{ field.description }}</span>\n }\n </div>\n </div>\n </mat-option>\n }\n </mat-select>\n } @else {\n <mat-select [ngModel]=\"metric.field || ''\" (ngModelChange)=\"setMetricField($index, $event)\" [disabled]=\"true\"></mat-select>\n }\n @if (!fieldOptions('metric').length) {\n <mat-hint>{{ t('praxis.charts.editor.catalog.metricFieldsMissing', 'No governed metrics are available for the current resource and operation.') }}</mat-hint>\n }\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\" [attr.data-testid]=\"'chart-editor-metric-label-' + $index\">\n <mat-label>{{ t('praxis.charts.editor.field.metricLabel', 'Metric label') }}</mat-label>\n <input matInput [ngModel]=\"metric.label || ''\" (ngModelChange)=\"setMetricLabel($index, $event)\" [disabled]=\"isReadonly()\" />\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\" [attr.data-testid]=\"'chart-editor-metric-aggregation-' + $index\">\n <mat-label>{{ t('praxis.charts.editor.field.metricAggregation', 'Aggregation') }}</mat-label>\n <mat-select [ngModel]=\"metric.aggregation || 'sum'\" (ngModelChange)=\"setMetricAggregation($index, $event)\" [disabled]=\"isReadonly()\">\n @for (aggregation of metricAggregationOptions(metric.field); track aggregation) {\n <mat-option [value]=\"aggregation\">\n {{ t('praxis.charts.editor.metricAggregation.' + aggregation, aggregation) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n @if (showMetricAxisControls()) {\n <mat-form-field class=\"editor-field\" appearance=\"outline\" [attr.data-testid]=\"'chart-editor-metric-axis-' + $index\">\n <mat-label>{{ t('praxis.charts.editor.field.metricAxis', 'Axis') }}</mat-label>\n <mat-select [ngModel]=\"metric.axis || 'primary'\" (ngModelChange)=\"setMetricAxis($index, $event)\" [disabled]=\"isReadonly()\">\n @for (axis of metricAxes; track axis) {\n <mat-option [value]=\"axis\">\n {{ t('praxis.charts.editor.metricAxis.' + axis, axis) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n }\n\n @if (showMetricSeriesKindControls()) {\n <mat-form-field class=\"editor-field\" appearance=\"outline\" [attr.data-testid]=\"'chart-editor-metric-series-kind-' + $index\">\n <mat-label>{{ t('praxis.charts.editor.field.metricSeriesKind', 'Series kind') }}</mat-label>\n <mat-select [ngModel]=\"metric.seriesKind || 'bar'\" (ngModelChange)=\"setMetricSeriesKind($index, $event)\" [disabled]=\"isReadonly()\">\n @for (seriesKind of metricSeriesKinds; track seriesKind) {\n <mat-option [value]=\"seriesKind\">\n {{ t('praxis.charts.editor.metricSeriesKind.' + seriesKind, seriesKind) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n }\n </div>\n\n <div class=\"editor-row-actions\">\n <button mat-button type=\"button\" [attr.data-testid]=\"'chart-editor-remove-metric-' + $index\" (click)=\"removeMetric($index)\" [disabled]=\"isReadonly() || metrics().length <= 1\">\n {{ t('praxis.charts.editor.analytics.removeMetric', 'Remove metric') }}\n </button>\n </div>\n </div>\n }\n\n <div>\n <button mat-stroked-button type=\"button\" data-testid=\"chart-editor-add-metric\" (click)=\"addMetric()\" [disabled]=\"isReadonly() || !canAddMetric()\">\n {{ t('praxis.charts.editor.analytics.addMetric', 'Add metric') }}\n </button>\n </div>\n </mat-card-content>\n </mat-card>\n </div>\n }\n\n @case ('events') {\n <div class=\"editor-stack\">\n <mat-card class=\"editor-card\" data-testid=\"chart-editor-event-pointClick\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.events.pointClickTitle', 'Point click') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventAction', 'Action') }}</mat-label>\n <mat-select data-testid=\"chart-editor-event-pointClick-action\" [ngModel]=\"eventAction('pointClick')\" (ngModelChange)=\"setEventAction('pointClick', $event)\" [disabled]=\"isReadonly()\">\n <mat-option value=\"\">{{ t('praxis.charts.editor.events.none', 'None') }}</mat-option>\n @for (action of eventActionOptions; track action) {\n <mat-option [value]=\"action\">\n {{ t('praxis.charts.editor.eventAction.' + action, action) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventTarget', 'Target') }}</mat-label>\n <mat-select data-testid=\"chart-editor-event-pointClick-target\" [ngModel]=\"eventTarget('pointClick')\" (ngModelChange)=\"setEventTarget('pointClick', $event)\" [disabled]=\"isReadonly() || !eventAction('pointClick') || targetCatalogUnavailable(eventAction('pointClick'), 'pointClick')\">\n @for (target of targetOptions(eventAction('pointClick'), 'pointClick'); track target.id) {\n <mat-option [value]=\"target.id\">{{ target.label }}</mat-option>\n }\n </mat-select>\n @if (targetCatalogUnavailable(eventAction('pointClick'), 'pointClick')) {\n <mat-hint>{{ t('praxis.charts.editor.catalog.targetMissing', 'Target catalog is required for this governed event action.') }}</mat-hint>\n }\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventMapping', 'Mapping') }}</mat-label>\n <textarea\n matInput\n data-testid=\"chart-editor-event-pointClick-mapping\"\n rows=\"4\"\n [ngModel]=\"eventMappingText('pointClick')\"\n (ngModelChange)=\"setEventMapping('pointClick', $event)\"\n [disabled]=\"isReadonly() || !eventAction('pointClick')\"\n ></textarea>\n </mat-form-field>\n </mat-card-content>\n </mat-card>\n\n <mat-card class=\"editor-card\" data-testid=\"chart-editor-event-selectionChange\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.events.selectionChangeTitle', 'Selection change') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventAction', 'Action') }}</mat-label>\n <mat-select data-testid=\"chart-editor-event-selectionChange-action\" [ngModel]=\"eventAction('selectionChange')\" (ngModelChange)=\"setEventAction('selectionChange', $event)\" [disabled]=\"isReadonly()\">\n <mat-option value=\"\">{{ t('praxis.charts.editor.events.none', 'None') }}</mat-option>\n @for (action of eventActionOptions; track action) {\n <mat-option [value]=\"action\">\n {{ t('praxis.charts.editor.eventAction.' + action, action) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventTarget', 'Target') }}</mat-label>\n <mat-select data-testid=\"chart-editor-event-selectionChange-target\" [ngModel]=\"eventTarget('selectionChange')\" (ngModelChange)=\"setEventTarget('selectionChange', $event)\" [disabled]=\"isReadonly() || !eventAction('selectionChange') || targetCatalogUnavailable(eventAction('selectionChange'), 'selectionChange')\">\n @for (target of targetOptions(eventAction('selectionChange'), 'selectionChange'); track target.id) {\n <mat-option [value]=\"target.id\">{{ target.label }}</mat-option>\n }\n </mat-select>\n @if (targetCatalogUnavailable(eventAction('selectionChange'), 'selectionChange')) {\n <mat-hint>{{ t('praxis.charts.editor.catalog.targetMissing', 'Target catalog is required for this governed event action.') }}</mat-hint>\n }\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventMapping', 'Mapping') }}</mat-label>\n <textarea\n matInput\n data-testid=\"chart-editor-event-selectionChange-mapping\"\n rows=\"4\"\n [ngModel]=\"eventMappingText('selectionChange')\"\n (ngModelChange)=\"setEventMapping('selectionChange', $event)\"\n [disabled]=\"isReadonly() || !eventAction('selectionChange')\"\n ></textarea>\n </mat-form-field>\n </mat-card-content>\n </mat-card>\n\n <mat-card class=\"editor-card\" data-testid=\"chart-editor-event-drillDown\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.events.drillDownTitle', 'Drill down') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventAction', 'Action') }}</mat-label>\n <mat-select data-testid=\"chart-editor-event-drillDown-action\" [ngModel]=\"eventAction('drillDown')\" (ngModelChange)=\"setEventAction('drillDown', $event)\" [disabled]=\"isReadonly()\">\n <mat-option value=\"\">{{ t('praxis.charts.editor.events.none', 'None') }}</mat-option>\n @for (action of eventActionOptions; track action) {\n <mat-option [value]=\"action\">\n {{ t('praxis.charts.editor.eventAction.' + action, action) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventTarget', 'Target') }}</mat-label>\n <mat-select data-testid=\"chart-editor-event-drillDown-target\" [ngModel]=\"eventTarget('drillDown')\" (ngModelChange)=\"setEventTarget('drillDown', $event)\" [disabled]=\"isReadonly() || !eventAction('drillDown') || targetCatalogUnavailable(eventAction('drillDown'), 'drillDown')\">\n @for (target of targetOptions(eventAction('drillDown'), 'drillDown'); track target.id) {\n <mat-option [value]=\"target.id\">{{ target.label }}</mat-option>\n }\n </mat-select>\n @if (targetCatalogUnavailable(eventAction('drillDown'), 'drillDown')) {\n <mat-hint>{{ t('praxis.charts.editor.catalog.targetMissing', 'Target catalog is required for this governed event action.') }}</mat-hint>\n }\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventMapping', 'Mapping') }}</mat-label>\n <textarea\n matInput\n data-testid=\"chart-editor-event-drillDown-mapping\"\n rows=\"4\"\n [ngModel]=\"eventMappingText('drillDown')\"\n (ngModelChange)=\"setEventMapping('drillDown', $event)\"\n [disabled]=\"isReadonly() || !eventAction('drillDown')\"\n ></textarea>\n </mat-form-field>\n </mat-card-content>\n </mat-card>\n\n <mat-card class=\"editor-card\" data-testid=\"chart-editor-event-crossFilter\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.events.crossFilterTitle', 'Cross filter') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventAction', 'Action') }}</mat-label>\n <mat-select data-testid=\"chart-editor-event-crossFilter-action\" [ngModel]=\"eventAction('crossFilter')\" (ngModelChange)=\"setEventAction('crossFilter', $event)\" [disabled]=\"isReadonly()\">\n <mat-option value=\"\">{{ t('praxis.charts.editor.events.none', 'None') }}</mat-option>\n @for (action of eventActionOptions; track action) {\n <mat-option [value]=\"action\">\n {{ t('praxis.charts.editor.eventAction.' + action, action) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventTarget', 'Target') }}</mat-label>\n <mat-select data-testid=\"chart-editor-event-crossFilter-target\" [ngModel]=\"eventTarget('crossFilter')\" (ngModelChange)=\"setEventTarget('crossFilter', $event)\" [disabled]=\"isReadonly() || !eventAction('crossFilter') || targetCatalogUnavailable(eventAction('crossFilter'), 'crossFilter')\">\n @for (target of targetOptions(eventAction('crossFilter'), 'crossFilter'); track target.id) {\n <mat-option [value]=\"target.id\">{{ target.label }}</mat-option>\n }\n </mat-select>\n @if (targetCatalogUnavailable(eventAction('crossFilter'), 'crossFilter')) {\n <mat-hint>{{ t('praxis.charts.editor.catalog.targetMissing', 'Target catalog is required for this governed event action.') }}</mat-hint>\n }\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventMapping', 'Mapping') }}</mat-label>\n <textarea\n matInput\n data-testid=\"chart-editor-event-crossFilter-mapping\"\n rows=\"4\"\n [ngModel]=\"eventMappingText('crossFilter')\"\n (ngModelChange)=\"setEventMapping('crossFilter', $event)\"\n [disabled]=\"isReadonly() || !eventAction('crossFilter')\"\n ></textarea>\n </mat-form-field>\n </mat-card-content>\n </mat-card>\n </div>\n }\n\n @case ('preview') {\n <mat-card class=\"editor-card\">\n <mat-card-content>\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 </mat-card-content>\n </mat-card>\n }\n }\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\" [class.editor-issue--warning]=\"issue.severity === 'warning'\">\n <div class=\"editor-issue__header\">\n <strong>{{ issue.field }}</strong>\n @if (issueTargetSection(issue); as targetSection) {\n <button\n mat-button\n type=\"button\"\n class=\"editor-issue__jump\"\n [attr.data-testid]=\"'chart-editor-issue-jump-' + issue.code\"\n [attr.aria-label]=\"t('praxis.charts.editor.issues.jumpToSection', 'Go to {{ section }}', { section: sectionLabel(targetSection) })\"\n (click)=\"setSection(targetSection)\"\n >\n {{ sectionLabel(targetSection) }} \u2192\n </button>\n }\n </div>\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()) {\n <div\n class=\"editor-empty editor-empty--actionable editor-preview-degraded\"\n data-testid=\"chart-editor-preview-degraded-warning\"\n >\n <span>{{\n effectivePreview()\n ? t('praxis.charts.editor.preview.degradedNotice', 'Aesthetic simulation active with synthetic sample data while contract validation issues remain open.')\n : t('praxis.charts.editor.preview.invalid', 'Preview is unavailable while the contract has blocking errors.')\n }}</span>\n @if (firstInvalidSection(); as invalidSection) {\n <button\n mat-stroked-button\n type=\"button\"\n data-testid=\"chart-editor-preview-jump-invalid\"\n [attr.aria-label]=\"t('praxis.charts.editor.issues.jumpToSection', 'Go to {{ section }}', { section: sectionLabel(invalidSection) })\"\n (click)=\"setSection(invalidSection)\"\n >\n {{ sectionLabel(invalidSection) }} \u2192\n </button>\n }\n </div>\n }\n @if (effectivePreview(); 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 <p class=\"editor-caption\" data-testid=\"chart-editor-preview-origin\">\n {{\n doc().source.kind === 'praxis.stats'\n ? t('praxis.charts.editor.preview.originStatsSynthetic', 'Previewing synthetic sample for governed backend stats (praxis.stats).')\n : chartPreview.dataOrigin === 'local'\n ? t('praxis.charts.editor.preview.originLocal', 'Previewing widget local dataset (inputs.data).')\n : t('praxis.charts.editor.preview.originSynthetic', 'Previewing synthetic sample data (no local dataset records).')\n }}\n </p>\n <praxis-chart [config]=\"chartPreview.config\" [data]=\"chartPreview.data\"></praxis-chart>\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:16px;min-width:0}.editor-nav{display:flex;gap:6px;flex-wrap:wrap;padding:4px;border-radius:14px;background:var(--md-sys-color-surface-container-low, rgba(18, 99, 180, .04));border:1px solid color-mix(in srgb,var(--md-sys-color-outline-variant, var(--md-sys-color-outline, #c5c7ce)) 64%,transparent)}.editor-nav button{border-radius:10px;border-color:transparent;display:inline-flex;align-items:center;gap:6px}.editor-nav button.active{background:color-mix(in srgb,var(--pfx-settings-panel-primary, var(--md-sys-color-primary, #1263b4)) 22%,transparent);color:var(--pfx-settings-panel-primary, var(--md-sys-color-primary, #1263b4));border-color:color-mix(in srgb,var(--pfx-settings-panel-primary, var(--md-sys-color-primary, #1263b4)) 48%,transparent);font-weight:600}.editor-nav__badge{display:inline-flex;align-items:center;justify-content:center;min-width:18px;height:18px;padding:0 5px;margin-left:6px;border-radius:999px;font-size:.7rem;font-weight:700;line-height:1;background:color-mix(in srgb,var(--md-sys-color-tertiary, #f59e0b) 24%,transparent);color:var(--pfx-settings-panel-fg, var(--md-sys-color-on-surface, inherit))}.editor-nav__badge--error{background:color-mix(in srgb,var(--pfx-settings-panel-error, var(--md-sys-color-error, #b3261e)) 24%,transparent);color:var(--pfx-settings-panel-error, var(--md-sys-color-error, #b3261e))}:host ::ng-deep .mat-mdc-form-field-hint-wrapper,:host ::ng-deep .mat-mdc-form-field-error-wrapper{position:relative!important;top:auto!important;left:auto!important;right:auto!important;padding-top:4px;line-height:1.35}:host ::ng-deep .mat-mdc-form-field-bottom-align:before{display:none}.editor-layout{display:grid;gap:16px;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:16px;border:1px solid color-mix(in srgb,var(--md-sys-color-outline-variant, var(--md-sys-color-outline, #c5c7ce)) 72%,transparent);background:var(--md-sys-color-surface-container-low, rgba(18, 99, 180, .03))}.editor-grid{display:grid;gap:14px;grid-template-columns:repeat(auto-fit,minmax(210px,1fr));align-items:start}.editor-toggle-cluster{display:flex;flex-wrap:wrap;align-items:center;gap:14px 24px}.editor-stack{display:grid;gap:14px}.editor-state-group{padding-top:14px;border-top:1px solid color-mix(in srgb,var(--md-sys-color-outline-variant, var(--md-sys-color-outline, #c5c7ce)) 48%,transparent)}.editor-state-group:first-child{padding-top:0;border-top:0}.editor-palette-swatches{display:flex;flex-wrap:wrap;align-items:center;gap:8px}.editor-palette-swatch-chip{display:inline-flex;align-items:center;gap:6px;padding:4px 8px;border-radius:999px;border:1px solid color-mix(in srgb,var(--md-sys-color-outline-variant, var(--md-sys-color-outline, #c5c7ce)) 68%,transparent);background:color-mix(in srgb,var(--pfx-settings-panel-surface, var(--md-sys-color-surface-container, transparent)) 88%,transparent);font-size:.76rem}.editor-palette-swatch-dot{width:16px;height:16px;border-radius:50%;border:1px solid rgba(128,128,128,.35);flex-shrink:0}.editor-palette-color-input{width:22px;height:22px;padding:0;border:0;border-radius:50%;background:transparent;cursor:pointer}.editor-palette-swatch-code{font-family:var(--pdx-font-mono, ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace);font-size:.74rem;color:var(--md-sys-color-on-surface, inherit)}.editor-palette-swatch-remove{appearance:none;border:0;background:transparent;color:var(--md-sys-color-on-surface-variant, #5a5d67);font-size:.95rem;line-height:1;padding:0 2px;cursor:pointer}.editor-row-card{padding:14px;border-radius:14px;border:1px solid color-mix(in srgb,var(--md-sys-color-outline-variant, var(--md-sys-color-outline, #c5c7ce)) 54%,transparent);background:color-mix(in srgb,var(--pfx-settings-panel-surface, var(--md-sys-color-surface-container, transparent)) 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:12px;background:color-mix(in srgb,var(--md-sys-color-error, #b3261e) 10%,transparent);border:1px solid color-mix(in srgb,var(--md-sys-color-error, #b3261e) 24%,transparent)}.editor-issue--warning{background:color-mix(in srgb,var(--md-sys-color-tertiary, #f59e0b) 10%,transparent);border-color:color-mix(in srgb,var(--md-sys-color-tertiary, #f59e0b) 26%,transparent)}.editor-issue__header{display:flex;align-items:center;justify-content:space-between;gap:8px;flex-wrap:wrap;margin-bottom:4px}.editor-issue strong{display:block;font-size:.84rem}.editor-issue__jump{min-height:26px;padding:0 8px;font-size:.76rem}.editor-caption{margin:0 0 12px;color:var(--md-sys-color-on-surface-variant, #5a5d67);font-size:.88rem;line-height:1.45}.editor-empty{padding:16px;border-radius:12px;background:color-mix(in srgb,var(--md-sys-color-surface-variant, #eceff4) 52%,transparent);border:1px dashed color-mix(in srgb,var(--md-sys-color-outline-variant, var(--md-sys-color-outline, #c5c7ce)) 64%,transparent);color:var(--md-sys-color-on-surface-variant, #5a5d67);font-size:.88rem}.editor-empty--actionable{display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap}.editor-select-trigger{display:inline-flex;align-items:center;gap:8px;min-width:0;max-width:100%}.editor-select-trigger__icon{width:18px;height:18px;font-size:18px;flex-shrink:0;color:var(--md-sys-color-primary, #1263b4)}.editor-select-trigger__label{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-weight:500}.editor-select-trigger__code{flex-shrink:0;max-width:45%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-family:var(--pdx-font-mono, ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace);font-size:.72rem;padding:1px 6px;border-radius:6px;background:color-mix(in srgb,currentColor 8%,transparent);color:color-mix(in srgb,currentColor 72%,transparent)}::ng-deep .editor-select-search{position:sticky;top:0;z-index:2;display:flex;align-items:center;gap:8px;padding:8px 12px;background:var(--md-sys-color-surface-container-highest, #f4f6fa);border-bottom:1px solid color-mix(in srgb,var(--md-sys-color-outline, #c5c7ce) 45%,transparent)}::ng-deep .editor-select-search mat-icon{width:18px;height:18px;font-size:18px;color:var(--md-sys-color-on-surface-variant, #5a5d67);flex-shrink:0}::ng-deep .editor-select-search input{width:100%;border:0;outline:none;background:transparent;color:inherit;font:inherit;font-size:.85rem}::ng-deep .mat-mdc-option.editor-rich-mat-option{min-height:54px;height:auto;padding-block:8px;align-items:flex-start}::ng-deep .editor-rich-option{display:flex;align-items:flex-start;gap:10px;min-width:0;width:100%;line-height:1.25}::ng-deep .editor-rich-option__icon{width:28px;height:28px;border-radius:8px;background:color-mix(in srgb,var(--md-sys-color-primary, #1263b4) 12%,transparent);color:var(--md-sys-color-primary, #1263b4);display:inline-flex;align-items:center;justify-content:center;flex-shrink:0;margin-top:1px}::ng-deep .editor-rich-option__icon mat-icon{width:16px;height:16px;font-size:16px}::ng-deep .editor-rich-option__body{display:flex;flex-direction:column;gap:2px;min-width:0;flex:1 1 auto}::ng-deep .editor-rich-option__header{display:flex;align-items:center;gap:6px;flex-wrap:wrap;min-width:0}::ng-deep .editor-rich-option__label{font-weight:600;font-size:.88rem;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}::ng-deep .editor-rich-option__badge,.editor-resource-summary__badge{display:inline-flex;align-items:center;padding:1px 6px;border-radius:999px;font-size:.68rem;font-weight:600;background:color-mix(in srgb,currentColor 10%,transparent);color:color-mix(in srgb,currentColor 78%,transparent);white-space:nowrap}::ng-deep .editor-rich-option__code,.editor-resource-summary__code{font-family:var(--pdx-font-mono, ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace);font-size:.72rem;color:var(--md-sys-color-primary, #1263b4);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}::ng-deep .editor-rich-option__description,.editor-resource-summary__description{margin:0;font-size:.75rem;color:var(--md-sys-color-on-surface-variant, #5a5d67);display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;white-space:normal}.editor-resource-summary{display:flex;align-items:flex-start;gap:10px;padding:10px 12px;border-radius:12px;background:color-mix(in srgb,var(--md-sys-color-primary, #1263b4) 6%,transparent);border:1px solid color-mix(in srgb,var(--md-sys-color-primary, #1263b4) 18%,transparent)}.editor-resource-summary__icon{width:28px;height:28px;border-radius:8px;background:color-mix(in srgb,var(--md-sys-color-primary, #1263b4) 12%,transparent);color:var(--md-sys-color-primary, #1263b4);display:inline-flex;align-items:center;justify-content:center;flex-shrink:0;margin-top:1px}.editor-resource-summary__icon mat-icon{width:16px;height:16px;font-size:16px}.editor-resource-summary__body{display:grid;gap:2px;min-width:0;flex:1 1 auto}.editor-resource-summary__header{display:flex;align-items:center;gap:6px;flex-wrap:wrap}.editor-side praxis-chart{display:block;width:100%;min-width:0;max-width:100%}.editor-preview-degraded{margin-bottom:12px;background:color-mix(in srgb,var(--md-sys-color-tertiary, #f59e0b) 10%,transparent);border-style:solid;border-color:color-mix(in srgb,var(--md-sys-color-tertiary, #f59e0b) 32%,transparent)}@container praxis-chart-editor (min-width: 841px){.editor-nav__preview-btn:not(.active){display:none}}@container praxis-chart-editor (max-width: 840px){.editor-layout{grid-template-columns:minmax(0,1fr)}.editor-shell--preview-active .editor-form{display:none}}@media(max-width:960px){.editor-layout{grid-template-columns:minmax(0,1fr)}}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$3.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$3.NumberValueAccessor, selector: "input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]" }, { kind: "directive", type: i1$3.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$3.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i2.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: MatCardModule }, { kind: "component", type: i3.MatCard, selector: "mat-card", inputs: ["appearance"], exportAs: ["matCard"] }, { kind: "directive", type: i3.MatCardContent, selector: "mat-card-content" }, { kind: "component", type: i3.MatCardHeader, selector: "mat-card-header" }, { kind: "directive", type: i3.MatCardTitle, selector: "mat-card-title, [mat-card-title], [matCardTitle]" }, { kind: "ngmodule", type: MatFormFieldModule }, { kind: "component", type: i4.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i4.MatLabel, selector: "mat-label" }, { kind: "directive", type: i4.MatHint, selector: "mat-hint", inputs: ["align", "id"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i5.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatInputModule }, { kind: "directive", type: i6.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: i7.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: "directive", type: i7.MatSelectTrigger, selector: "mat-select-trigger" }, { kind: "component", type: i7.MatOption, selector: "mat-option", inputs: ["value", "id", "disabled"], outputs: ["onSelectionChange"], exportAs: ["matOption"] }, { kind: "ngmodule", type: MatSlideToggleModule }, { kind: "component", type: i8.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: "directive", type: PraxisIconDirective, selector: "mat-icon[praxisIcon]", inputs: ["praxisIcon"] }, { kind: "component", type: PraxisChartComponent, selector: "praxis-chart", inputs: ["config", "resourceKey", "data", "chartDocument", "filterCriteria", "queryContext", "remoteDataResolver", "enableCustomization", "availableResources", "availableFields", "availableTargets"], outputs: ["pointClick", "pointAction", "selectionChange", "drillDown", "crossFilter", "queryRequest", "loadStateChange", "chartDocumentApplied", "chartDocumentSaved", "chartDocumentPatchChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
16608
|
+
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 }, localDataInput: { classPropertyName: "localDataInput", publicName: "localData", 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", localDataChange: "localDataChange" }, providers: [providePraxisChartsI18n()], ngImport: i0, template: "<div class=\"editor-shell\" [class.editor-shell--preview-active]=\"activeSection() === 'preview'\">\n <div class=\"editor-nav\" role=\"toolbar\" [attr.aria-label]=\"t('praxis.charts.editor.sectionsNav', 'Se\u00E7\u00F5es do editor')\">\n @for (section of sections; track section.id) {\n <button\n mat-stroked-button\n type=\"button\"\n [attr.id]=\"'chart-editor-tab-' + section.id\"\n aria-controls=\"chart-editor-tabpanel\"\n [attr.aria-pressed]=\"activeSection() === section.id\"\n [tabIndex]=\"activeSection() === section.id ? 0 : -1\"\n [attr.data-testid]=\"'chart-editor-section-' + section.id\"\n [class.active]=\"activeSection() === section.id\"\n [class.editor-nav__preview-btn]=\"section.id === 'preview'\"\n [class.editor-nav__has-error]=\"sectionHasError(section.id)\"\n (click)=\"setSection(section.id)\"\n (keydown)=\"onSectionNavKeydown($event, $index)\"\n >\n <span>{{ t(section.labelKey, section.fallback) }}</span>\n @if (sectionIssueCount(section.id) > 0) {\n <span\n class=\"editor-nav__badge\"\n aria-hidden=\"true\"\n [class.editor-nav__badge--error]=\"sectionHasError(section.id)\"\n [attr.data-testid]=\"'chart-editor-section-badge-' + section.id\"\n >\n {{ sectionIssueCount(section.id) }}\n </span>\n }\n </button>\n }\n </div>\n\n <div class=\"editor-layout\">\n <div\n class=\"editor-form\"\n id=\"chart-editor-tabpanel\"\n role=\"tabpanel\"\n [attr.aria-labelledby]=\"'chart-editor-tab-' + activeSection()\"\n >\n @switch (activeSection()) {\n @case ('general') {\n <mat-card class=\"editor-card\">\n <mat-card-content 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 </mat-card-content>\n </mat-card>\n }\n\n @case ('data') {\n <div class=\"editor-stack\">\n <mat-card class=\"editor-card\">\n <mat-card-content class=\"editor-stack\">\n <p class=\"editor-caption\" data-testid=\"chart-editor-effective-origin\">\n {{ effectiveDataOriginSummary() }}\n </p>\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\n data-testid=\"chart-editor-resource\"\n [ngModel]=\"resourceValue()\"\n (ngModelChange)=\"setResource($event)\"\n (openedChange)=\"setResourceSearchTerm('')\"\n [disabled]=\"isReadonly() || resourceCatalogUnavailable()\"\n >\n @if (selectedResourceOption(); as selectedRes) {\n <mat-select-trigger>\n <span class=\"editor-select-trigger\">\n <mat-icon class=\"editor-select-trigger__icon\" [praxisIcon]=\"selectedRes.icon || 'api'\"></mat-icon>\n <span class=\"editor-select-trigger__label\">{{ selectedRes.label }}</span>\n <span class=\"editor-select-trigger__code\">{{ selectedRes.path }}</span>\n </span>\n </mat-select-trigger>\n }\n <div class=\"editor-select-search\" (click)=\"$event.stopPropagation()\">\n <mat-icon aria-hidden=\"true\">search</mat-icon>\n <input\n type=\"search\"\n data-testid=\"chart-editor-resource-search\"\n [placeholder]=\"t('praxis.charts.editor.field.resourceSearchPlaceholder', 'Search by name, key, or endpoint...')\"\n [ngModel]=\"resourceSearchTerm()\"\n (ngModelChange)=\"setResourceSearchTerm($event)\"\n (keydown)=\"onSelectSearchKeydown($event)\"\n />\n </div>\n @for (resource of filteredResourceOptions(); track resource.id) {\n <mat-option [value]=\"resource.path\" class=\"editor-rich-mat-option\">\n <div class=\"editor-rich-option\">\n <span class=\"editor-rich-option__icon\" aria-hidden=\"true\">\n <mat-icon [praxisIcon]=\"resource.icon || 'api'\"></mat-icon>\n </span>\n <div class=\"editor-rich-option__body\">\n <div class=\"editor-rich-option__header\">\n <span class=\"editor-rich-option__label\">{{ resource.label }}</span>\n @if (resource.group) {\n <span class=\"editor-rich-option__badge\">{{ resource.group }}</span>\n }\n @if (resource.readOnly) {\n <span class=\"editor-rich-option__badge\">{{ t('praxis.charts.editor.catalog.readOnly', 'Read-only') }}</span>\n }\n </div>\n <span class=\"editor-rich-option__code\">\n {{ resource.path }}{{ resource.id && resource.id !== resource.path && resource.id !== resource.label ? ' \u00B7 ' + resource.id : '' }}\n </span>\n @if (resource.description) {\n <span class=\"editor-rich-option__description\">{{ resource.description }}</span>\n }\n </div>\n </div>\n </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() || isResolvingResource() || 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 (isResolvingResource()) {\n <mat-hint data-testid=\"chart-editor-resolving-resource\">\n {{ t('praxis.charts.editor.catalog.resolvingResource', 'Resolving governed stats operations and fields for the selected resource...') }}\n </mat-hint>\n } @else 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 (doc().source.kind === 'praxis.stats' && selectedResourceOption(); as selectedRes) {\n <div class=\"editor-resource-summary\" data-testid=\"chart-editor-resource-summary\">\n <span class=\"editor-resource-summary__icon\" aria-hidden=\"true\">\n <mat-icon [praxisIcon]=\"selectedRes.icon || 'api'\"></mat-icon>\n </span>\n <div class=\"editor-resource-summary__body\">\n <div class=\"editor-resource-summary__header\">\n <strong>{{ selectedRes.label }}</strong>\n @if (selectedRes.group) {\n <span class=\"editor-resource-summary__badge\">{{ selectedRes.group }}</span>\n }\n @if (selectedRes.readOnly) {\n <span class=\"editor-resource-summary__badge\">{{ t('praxis.charts.editor.catalog.readOnly', 'Read-only') }}</span>\n }\n </div>\n <span class=\"editor-resource-summary__code\">\n {{ selectedRes.path }}{{ selectedRes.id && selectedRes.id !== selectedRes.path && selectedRes.id !== selectedRes.label ? ' \u00B7 ' + selectedRes.id : '' }}\n </span>\n @if (selectedRes.description) {\n <p class=\"editor-resource-summary__description\">{{ selectedRes.description }}</p>\n }\n </div>\n </div>\n }\n </mat-card-content>\n </mat-card>\n\n @if (showTimeseriesControls()) {\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.specialization.timeseriesTitle', 'Timeseries options') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.granularity', 'Granularity') }}</mat-label>\n <mat-select [ngModel]=\"granularityValue()\" (ngModelChange)=\"setGranularity($event)\" [disabled]=\"isReadonly()\">\n @for (granularity of timeGranularities; track granularity) {\n <mat-option [value]=\"granularity\">\n {{ t('praxis.charts.editor.granularity.' + granularity, granularity) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n <mat-slide-toggle\n [ngModel]=\"fillGapsValue()\"\n (ngModelChange)=\"setFillGaps($event)\"\n [disabled]=\"isReadonly()\"\n >\n {{ t('praxis.charts.editor.field.fillGaps', 'Fill missing intervals') }}\n </mat-slide-toggle>\n </mat-card-content>\n </mat-card>\n }\n\n @if (showDistributionControls()) {\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.specialization.distributionTitle', 'Distribution options') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.distributionMode', 'Distribution mode') }}</mat-label>\n <mat-select data-testid=\"chart-editor-distribution-mode\" [ngModel]=\"distributionModeValue()\" (ngModelChange)=\"setDistributionMode($event)\" [disabled]=\"isReadonly()\">\n @for (mode of distributionModes; track mode) {\n <mat-option [value]=\"mode\">\n {{ t('praxis.charts.editor.distributionMode.' + mode, mode) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n @if (distributionModeValue() === 'histogram') {\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.bucketSize', 'Bucket size') }}</mat-label>\n <input matInput data-testid=\"chart-editor-bucket-size\" [ngModel]=\"bucketSizeValue()\" (ngModelChange)=\"setBucketSize($event)\" [disabled]=\"isReadonly()\" />\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.bucketCount', 'Bucket count') }}</mat-label>\n <input matInput data-testid=\"chart-editor-bucket-count\" [ngModel]=\"bucketCountValue()\" (ngModelChange)=\"setBucketCount($event)\" [disabled]=\"isReadonly()\" />\n </mat-form-field>\n }\n </mat-card-content>\n </mat-card>\n }\n\n @if (showComparisonControls()) {\n <mat-card class=\"editor-card\" data-testid=\"chart-editor-comparison-options\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.specialization.comparisonTitle', 'Comparison period') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.comparisonPeriodField', 'Period field') }}</mat-label>\n @if (fieldOptions('time').length) {\n <mat-select\n [ngModel]=\"comparisonPeriodFieldValue()\"\n (ngModelChange)=\"setComparisonPeriodField($event)\"\n (openedChange)=\"setFieldSearchTerm('')\"\n [disabled]=\"isReadonly()\"\n >\n <div class=\"editor-select-search\" (click)=\"$event.stopPropagation()\">\n <mat-icon aria-hidden=\"true\">search</mat-icon>\n <input\n type=\"search\"\n [placeholder]=\"t('praxis.charts.editor.field.fieldSearchPlaceholder', 'Search field...')\"\n [ngModel]=\"fieldSearchTerm()\"\n (ngModelChange)=\"setFieldSearchTerm($event)\"\n (keydown)=\"onSelectSearchKeydown($event)\"\n />\n </div>\n @for (field of filteredFieldOptions('time', comparisonPeriodFieldValue()); track field.field) {\n <mat-option [value]=\"field.field\" class=\"editor-rich-mat-option\">\n <div class=\"editor-rich-option\">\n <div class=\"editor-rich-option__body\">\n <div class=\"editor-rich-option__header\">\n <span class=\"editor-rich-option__label\">{{ field.label || field.field }}</span>\n @if (field.type) {\n <span class=\"editor-rich-option__badge\">{{ field.type }}</span>\n }\n </div>\n @if (field.label && field.label !== field.field) {\n <span class=\"editor-rich-option__code\">{{ field.field }}</span>\n }\n @if (field.description) {\n <span class=\"editor-rich-option__description\">{{ field.description }}</span>\n }\n </div>\n </div>\n </mat-option>\n }\n </mat-select>\n } @else {\n <mat-select [ngModel]=\"comparisonPeriodFieldValue()\" [disabled]=\"true\"></mat-select>\n }\n @if (!fieldOptions('time').length) {\n <mat-hint>{{ t('praxis.charts.editor.catalog.comparisonPeriodFieldsMissing', 'No governed time-series field is available for comparison periods.') }}</mat-hint>\n }\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.comparisonTimezone', 'Timezone') }}</mat-label>\n <input\n matInput\n [ngModel]=\"comparisonPeriodTimezoneValue()\"\n (ngModelChange)=\"setComparisonPeriodTimezone($event)\"\n [disabled]=\"isReadonly()\"\n />\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.comparisonPreset', 'Period preset') }}</mat-label>\n <mat-select\n [ngModel]=\"comparisonPeriodPresetValue()\"\n (ngModelChange)=\"setComparisonPeriodPreset($event)\"\n [disabled]=\"isReadonly()\"\n >\n @for (preset of comparisonPeriodPresets; track preset) {\n <mat-option [value]=\"preset\">\n {{ t('praxis.charts.editor.comparisonPreset.' + preset, preset) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.comparisonMode', 'Comparison mode') }}</mat-label>\n <mat-select\n [ngModel]=\"comparisonPeriodModeValue()\"\n (ngModelChange)=\"setComparisonPeriodMode($event)\"\n [disabled]=\"isReadonly()\"\n >\n @for (mode of comparisonPeriodModes; track mode) {\n <mat-option [value]=\"mode\">\n {{ t('praxis.charts.editor.comparisonMode.' + mode, mode) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n </mat-card-content>\n </mat-card>\n }\n\n @if (doc().source.kind === 'derived') {\n <mat-card class=\"editor-card\" data-testid=\"chart-editor-local-data\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.localData.title', 'Local dataset (inputs.data)') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-stack\">\n <p class=\"editor-caption\">\n {{ t('praxis.charts.editor.localData.caption', 'Edit the local records rendered when the data source is derived.') }}\n </p>\n\n @if (canConsolidateGaugeLocalData()) {\n <div class=\"editor-row-actions\">\n <button\n mat-stroked-button\n type=\"button\"\n data-testid=\"chart-local-data-consolidate-gauge\"\n (click)=\"consolidateGaugeLocalData()\"\n [disabled]=\"isReadonly()\"\n >\n {{ t('praxis.charts.editor.localData.consolidateGauge', 'Consolidate to 1 bucket for gauge') }}\n </button>\n </div>\n }\n\n @if (localDataRows().length) {\n @for (row of localDataRows(); track $index; let rowIndex = $index) {\n <div class=\"editor-row-card\" [attr.data-testid]=\"'chart-local-data-row-' + rowIndex\">\n <div class=\"editor-grid\">\n @for (dimField of localDataDimensionFields(); track dimField) {\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ dimField }}</mat-label>\n <input\n matInput\n [attr.data-testid]=\"'chart-local-data-dim-' + rowIndex + '-' + dimField\"\n [ngModel]=\"localDataCellStringValue(rowIndex, dimField)\"\n (ngModelChange)=\"setLocalDataCellValue(rowIndex, dimField, $event, 'dimension')\"\n [disabled]=\"isReadonly()\"\n />\n </mat-form-field>\n }\n @for (metricField of localDataMetricFields(); track metricField) {\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ metricField }}</mat-label>\n <input\n matInput\n type=\"number\"\n [attr.data-testid]=\"'chart-local-data-metric-' + rowIndex + '-' + metricField\"\n [ngModel]=\"localDataCellStringValue(rowIndex, metricField)\"\n (ngModelChange)=\"setLocalDataCellValue(rowIndex, metricField, $event, 'metric')\"\n [disabled]=\"isReadonly()\"\n />\n </mat-form-field>\n }\n </div>\n <div class=\"editor-row-actions\">\n <button\n mat-button\n type=\"button\"\n [attr.data-testid]=\"'chart-local-data-remove-row-' + rowIndex\"\n (click)=\"removeLocalDataRow(rowIndex)\"\n [disabled]=\"isReadonly()\"\n >\n {{ t('praxis.charts.editor.localData.removeRow', 'Remove record') }}\n </button>\n </div>\n </div>\n }\n } @else {\n <div class=\"editor-empty\" data-testid=\"chart-local-data-empty\">\n {{ t('praxis.charts.editor.localData.empty', 'No local records configured yet. Add a record to replace the synthetic preview sample.') }}\n </div>\n }\n\n <div>\n <button\n mat-stroked-button\n type=\"button\"\n data-testid=\"chart-local-data-add-row\"\n (click)=\"addLocalDataRow()\"\n [disabled]=\"isReadonly()\"\n >\n {{ t('praxis.charts.editor.localData.addRow', 'Add record') }}\n </button>\n </div>\n </mat-card-content>\n </mat-card>\n }\n </div>\n }\n\n @case ('motion') {\n <mat-card class=\"editor-card\">\n <mat-card-content class=\"editor-stack\">\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 <p class=\"editor-caption\" data-testid=\"chart-editor-motion-hint\">\n {{ t('praxis.charts.editor.motion.hint', 'Controls initial entrance timing and smooth transitions when filters or query context change, with automatic volume thresholds for high-density datasets.') }}\n </p>\n\n <div>\n <button\n mat-stroked-button\n type=\"button\"\n data-testid=\"chart-editor-simulate-motion\"\n (click)=\"simulateMotionTransition()\"\n [disabled]=\"normalizedDocument().motion?.enabled === false || !effectivePreview()\"\n >\n {{ t('praxis.charts.editor.motion.simulate', 'Simulate transition') }}\n </button>\n </div>\n </mat-card-content>\n </mat-card>\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-toggle-cluster\">\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 <div class=\"editor-grid\">\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 </div>\n\n @if (canApplyDomainSemanticPalette()) {\n <div class=\"editor-row-actions\">\n <button\n mat-stroked-button\n type=\"button\"\n data-testid=\"chart-editor-apply-domain-palette\"\n (click)=\"applyDomainSemanticPalette()\"\n [disabled]=\"isReadonly()\"\n [title]=\"t('praxis.charts.editor.appearance.applyDomainPaletteHint', 'Generates a custom palette with semantic colors (success, warning, danger, info) from the dimension categories.')\"\n >\n <mat-icon aria-hidden=\"true\">auto_fix_high</mat-icon>\n {{ t('praxis.charts.editor.appearance.applyDomainPalette', 'Apply domain semantic colors') }}\n </button>\n </div>\n }\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\n <div class=\"editor-palette-swatches\" data-testid=\"chart-editor-palette-swatches\">\n @for (swatch of tokenPaletteSwatches(); track $index) {\n <span class=\"editor-palette-swatch-chip\" [title]=\"swatch\">\n <span class=\"editor-palette-swatch-dot\" [style.background]=\"swatch\"></span>\n <code class=\"editor-palette-swatch-code\">{{ swatch }}</code>\n </span>\n }\n </div>\n } @else {\n <div class=\"editor-palette-swatches\" data-testid=\"chart-editor-palette-swatches\">\n @for (swatch of customPaletteColors(); track $index) {\n <span class=\"editor-palette-swatch-chip\" [title]=\"swatch\">\n <span class=\"editor-palette-swatch-dot\" [style.background]=\"swatch\"></span>\n <input\n type=\"color\"\n class=\"editor-palette-color-input\"\n [attr.data-testid]=\"'chart-editor-palette-color-' + $index\"\n [value]=\"swatchHexValue(swatch, $index)\"\n (input)=\"setCustomPaletteColorAt($index, $any($event.target).value)\"\n [disabled]=\"isReadonly()\"\n />\n <code class=\"editor-palette-swatch-code\">{{ swatch }}</code>\n <button\n type=\"button\"\n class=\"editor-palette-swatch-remove\"\n [attr.data-testid]=\"'chart-editor-palette-remove-' + $index\"\n (click)=\"removeCustomPaletteColor($index)\"\n [disabled]=\"isReadonly()\"\n [attr.aria-label]=\"t('praxis.charts.editor.appearance.removePaletteColor', 'Remove color')\"\n >\n \u00D7\n </button>\n </span>\n }\n <button\n mat-stroked-button\n type=\"button\"\n class=\"editor-palette-add-btn\"\n data-testid=\"chart-editor-add-palette-color\"\n (click)=\"addCustomPaletteColor()\"\n [disabled]=\"isReadonly()\"\n >\n + {{ t('praxis.charts.editor.appearance.addPaletteColor', 'Add color') }}\n </button>\n </div>\n\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=\"2\"\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-state-group\">\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=\"1\" [ngModel]=\"stateDescription('empty')\" (ngModelChange)=\"setStateDescription('empty', $event)\" [disabled]=\"isReadonly()\"></textarea>\n </mat-form-field>\n </div>\n </div>\n\n <div class=\"editor-state-group\">\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=\"1\" [ngModel]=\"stateDescription('loading')\" (ngModelChange)=\"setStateDescription('loading', $event)\" [disabled]=\"isReadonly()\"></textarea>\n </mat-form-field>\n </div>\n </div>\n\n <div class=\"editor-state-group\">\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=\"1\" [ngModel]=\"stateDescription('error')\" (ngModelChange)=\"setStateDescription('error', $event)\" [disabled]=\"isReadonly()\"></textarea>\n </mat-form-field>\n </div>\n </div>\n </mat-card-content>\n </mat-card>\n </div>\n }\n\n @case ('analytics') {\n <div class=\"editor-stack\">\n @if (showGaugePanel()) {\n <mat-card class=\"editor-card\" data-testid=\"chart-editor-gauge-scale\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.specialization.gaugeTitle', 'Gauge scale') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-stack\">\n <p class=\"editor-caption\">\n {{ t('praxis.charts.editor.specialization.gaugeHint', 'Gauge charts render exactly one bucket and one metric. Remote stats use group-by with limit 1; values outside the scale fail instead of being clamped.') }}\n </p>\n <div class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.gaugeMin', 'Scale minimum') }}</mat-label>\n <input\n matInput\n type=\"number\"\n data-testid=\"chart-editor-gauge-min\"\n [ngModel]=\"gaugeMinValue()\"\n (ngModelChange)=\"setGaugeMin($event)\"\n [disabled]=\"isReadonly()\"\n />\n </mat-form-field>\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.gaugeMax', 'Scale maximum') }}</mat-label>\n <input\n matInput\n type=\"number\"\n data-testid=\"chart-editor-gauge-max\"\n [ngModel]=\"gaugeMaxValue()\"\n (ngModelChange)=\"setGaugeMax($event)\"\n [disabled]=\"isReadonly()\"\n />\n </mat-form-field>\n </div>\n </mat-card-content>\n </mat-card>\n }\n\n @if (showComboPanel()) {\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.specialization.comboTitle', 'Combo guidance') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content>\n <p class=\"editor-caption\">\n {{ t('praxis.charts.editor.specialization.comboHint', 'Combo charts require at least two metrics and allow per-metric axis and series kind mapping.') }}\n </p>\n </mat-card-content>\n </mat-card>\n }\n\n @if (showPieDonutPanel()) {\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.specialization.pieDonutTitle', 'Composition guidance') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content>\n <p class=\"editor-caption\">\n {{ t('praxis.charts.editor.specialization.pieDonutHint', 'Pie, donut, funnel, pyramid and treemap charts keep one metric and use the first dimension as the category segment.') }}\n </p>\n </mat-card-content>\n </mat-card>\n }\n\n @if (showScatterPanel()) {\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.specialization.scatterTitle', 'Scatter guidance') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content>\n <p class=\"editor-caption\">\n {{ t('praxis.charts.editor.specialization.scatterHint', 'With one metric, scatter uses the first dimension as X and the metric as Y. With two metrics, the dimension identifies the grouped bucket and the metrics map to X and Y.') }}\n </p>\n </mat-card-content>\n </mat-card>\n }\n\n @if (showHeatmapPanel()) {\n <mat-card class=\"editor-card\" data-testid=\"chart-editor-heatmap-panel\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.specialization.heatmapTitle', 'Heatmap matrix guidance') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content>\n <p class=\"editor-caption\">\n {{ t('praxis.charts.editor.specialization.heatmapHint', 'Heatmap charts render a 2D matrix requiring two dimensions (X and Y axes) and one metric, using group-by for remote stats.') }}\n </p>\n </mat-card-content>\n </mat-card>\n }\n\n @if (canApplySemanticAnalyticsMapping()) {\n <div class=\"editor-row-actions\">\n <button\n mat-stroked-button\n type=\"button\"\n data-testid=\"chart-editor-smart-map-analytics\"\n (click)=\"applySemanticAnalyticsMapping()\"\n [disabled]=\"isReadonly()\"\n [title]=\"t('praxis.charts.editor.analytics.smartMapHint', 'Automatically selects the best dimension, metric, and aggregation from the governed field catalog.')\"\n >\n <mat-icon aria-hidden=\"true\">auto_fix_high</mat-icon>\n {{ t('praxis.charts.editor.analytics.smartMap', 'Suggest dimension & metric from schema') }}\n </button>\n </div>\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\n [ngModel]=\"dimension.field || ''\"\n (ngModelChange)=\"setDimensionField($index, $event)\"\n (openedChange)=\"setFieldSearchTerm('')\"\n [disabled]=\"isReadonly()\"\n >\n <div class=\"editor-select-search\" (click)=\"$event.stopPropagation()\">\n <mat-icon aria-hidden=\"true\">search</mat-icon>\n <input\n type=\"search\"\n [placeholder]=\"t('praxis.charts.editor.field.fieldSearchPlaceholder', 'Search field...')\"\n [ngModel]=\"fieldSearchTerm()\"\n (ngModelChange)=\"setFieldSearchTerm($event)\"\n (keydown)=\"onSelectSearchKeydown($event)\"\n />\n </div>\n @for (field of filteredFieldOptions('dimension', dimension.field); track field.field) {\n <mat-option [value]=\"field.field\" class=\"editor-rich-mat-option\">\n <div class=\"editor-rich-option\">\n <div class=\"editor-rich-option__body\">\n <div class=\"editor-rich-option__header\">\n <span class=\"editor-rich-option__label\">{{ field.label || field.field }}</span>\n @if (field.type) {\n <span class=\"editor-rich-option__badge\">{{ field.type }}</span>\n }\n </div>\n @if (field.label && field.label !== field.field) {\n <span class=\"editor-rich-option__code\">{{ field.field }}</span>\n }\n @if (field.description) {\n <span class=\"editor-rich-option__description\">{{ field.description }}</span>\n }\n </div>\n </div>\n </mat-option>\n }\n </mat-select>\n } @else {\n <mat-select [ngModel]=\"dimension.field || ''\" (ngModelChange)=\"setDimensionField($index, $event)\" [disabled]=\"true\"></mat-select>\n }\n @if (!fieldOptions('dimension').length) {\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\n <mat-form-field class=\"editor-field\" appearance=\"outline\" [attr.data-testid]=\"'chart-editor-dimension-extraction-' + $index\">\n <mat-label>{{ t('praxis.charts.editor.field.dimensionExtraction', 'Temporal extraction') }}</mat-label>\n <mat-select [ngModel]=\"dimension.extraction || ''\" (ngModelChange)=\"setDimensionExtraction($index, $event)\" [disabled]=\"isReadonly()\">\n <mat-option [value]=\"''\">\n {{ t('praxis.charts.editor.dimensionExtraction.none', 'None') }}\n </mat-option>\n @for (extraction of dimensionExtractions; track extraction) {\n <mat-option [value]=\"extraction\">\n {{ t('praxis.charts.editor.dimensionExtraction.' + extraction, extraction) }}\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() || !canRemoveDimension()\">\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() || !canAddDimension()\">\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\n [ngModel]=\"metric.field || ''\"\n (ngModelChange)=\"setMetricField($index, $event)\"\n (openedChange)=\"setFieldSearchTerm('')\"\n [disabled]=\"isReadonly()\"\n >\n <div class=\"editor-select-search\" (click)=\"$event.stopPropagation()\">\n <mat-icon aria-hidden=\"true\">search</mat-icon>\n <input\n type=\"search\"\n [placeholder]=\"t('praxis.charts.editor.field.fieldSearchPlaceholder', 'Search field...')\"\n [ngModel]=\"fieldSearchTerm()\"\n (ngModelChange)=\"setFieldSearchTerm($event)\"\n (keydown)=\"onSelectSearchKeydown($event)\"\n />\n </div>\n @for (field of filteredFieldOptions('metric', metric.field); track field.field) {\n <mat-option [value]=\"field.field\" class=\"editor-rich-mat-option\">\n <div class=\"editor-rich-option\">\n <div class=\"editor-rich-option__body\">\n <div class=\"editor-rich-option__header\">\n <span class=\"editor-rich-option__label\">{{ field.label || field.field }}</span>\n @if (field.type) {\n <span class=\"editor-rich-option__badge\">{{ field.type }}</span>\n }\n </div>\n @if (field.label && field.label !== field.field) {\n <span class=\"editor-rich-option__code\">{{ field.field }}</span>\n }\n @if (field.description) {\n <span class=\"editor-rich-option__description\">{{ field.description }}</span>\n }\n </div>\n </div>\n </mat-option>\n }\n </mat-select>\n } @else {\n <mat-select [ngModel]=\"metric.field || ''\" (ngModelChange)=\"setMetricField($index, $event)\" [disabled]=\"true\"></mat-select>\n }\n @if (!fieldOptions('metric').length) {\n <mat-hint>{{ t('praxis.charts.editor.catalog.metricFieldsMissing', 'No governed metrics are available for the current resource and operation.') }}</mat-hint>\n }\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\" [attr.data-testid]=\"'chart-editor-metric-label-' + $index\">\n <mat-label>{{ t('praxis.charts.editor.field.metricLabel', 'Metric label') }}</mat-label>\n <input matInput [ngModel]=\"metric.label || ''\" (ngModelChange)=\"setMetricLabel($index, $event)\" [disabled]=\"isReadonly()\" />\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\" [attr.data-testid]=\"'chart-editor-metric-aggregation-' + $index\">\n <mat-label>{{ t('praxis.charts.editor.field.metricAggregation', 'Aggregation') }}</mat-label>\n <mat-select [ngModel]=\"metric.aggregation || 'sum'\" (ngModelChange)=\"setMetricAggregation($index, $event)\" [disabled]=\"isReadonly()\">\n @for (aggregation of metricAggregationOptions(metric.field); track aggregation) {\n <mat-option [value]=\"aggregation\">\n {{ t('praxis.charts.editor.metricAggregation.' + aggregation, aggregation) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n @if (showMetricAxisControls()) {\n <mat-form-field class=\"editor-field\" appearance=\"outline\" [attr.data-testid]=\"'chart-editor-metric-axis-' + $index\">\n <mat-label>{{ t('praxis.charts.editor.field.metricAxis', 'Axis') }}</mat-label>\n <mat-select [ngModel]=\"metric.axis || 'primary'\" (ngModelChange)=\"setMetricAxis($index, $event)\" [disabled]=\"isReadonly()\">\n @for (axis of metricAxes; track axis) {\n <mat-option [value]=\"axis\">\n {{ t('praxis.charts.editor.metricAxis.' + axis, axis) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n }\n\n @if (showMetricSeriesKindControls()) {\n <mat-form-field class=\"editor-field\" appearance=\"outline\" [attr.data-testid]=\"'chart-editor-metric-series-kind-' + $index\">\n <mat-label>{{ t('praxis.charts.editor.field.metricSeriesKind', 'Series kind') }}</mat-label>\n <mat-select [ngModel]=\"metric.seriesKind || 'bar'\" (ngModelChange)=\"setMetricSeriesKind($index, $event)\" [disabled]=\"isReadonly()\">\n @for (seriesKind of metricSeriesKinds; track seriesKind) {\n <mat-option [value]=\"seriesKind\">\n {{ t('praxis.charts.editor.metricSeriesKind.' + seriesKind, seriesKind) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n }\n </div>\n\n <div class=\"editor-row-actions\">\n <button mat-button type=\"button\" [attr.data-testid]=\"'chart-editor-remove-metric-' + $index\" (click)=\"removeMetric($index)\" [disabled]=\"isReadonly() || metrics().length <= 1\">\n {{ t('praxis.charts.editor.analytics.removeMetric', 'Remove metric') }}\n </button>\n </div>\n </div>\n }\n\n <div>\n <button mat-stroked-button type=\"button\" data-testid=\"chart-editor-add-metric\" (click)=\"addMetric()\" [disabled]=\"isReadonly() || !canAddMetric()\">\n {{ t('praxis.charts.editor.analytics.addMetric', 'Add metric') }}\n </button>\n </div>\n </mat-card-content>\n </mat-card>\n </div>\n }\n\n @case ('events') {\n <div class=\"editor-stack\">\n <mat-card class=\"editor-card\" data-testid=\"chart-editor-event-pointClick\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.events.pointClickTitle', 'Point click') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventAction', 'Action') }}</mat-label>\n <mat-select data-testid=\"chart-editor-event-pointClick-action\" [ngModel]=\"eventAction('pointClick')\" (ngModelChange)=\"setEventAction('pointClick', $event)\" [disabled]=\"isReadonly()\">\n <mat-option value=\"\">{{ t('praxis.charts.editor.events.none', 'None') }}</mat-option>\n @for (action of eventActionOptions; track action) {\n <mat-option [value]=\"action\">\n {{ t('praxis.charts.editor.eventAction.' + action, action) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventTarget', 'Target') }}</mat-label>\n <mat-select data-testid=\"chart-editor-event-pointClick-target\" [ngModel]=\"eventTarget('pointClick')\" (ngModelChange)=\"setEventTarget('pointClick', $event)\" [disabled]=\"isReadonly() || !eventAction('pointClick') || targetCatalogUnavailable(eventAction('pointClick'), 'pointClick')\">\n @for (target of targetOptions(eventAction('pointClick'), 'pointClick'); track target.id) {\n <mat-option [value]=\"target.id\">{{ target.label }}</mat-option>\n }\n </mat-select>\n @if (targetCatalogUnavailable(eventAction('pointClick'), 'pointClick')) {\n <mat-hint>{{ t('praxis.charts.editor.catalog.targetMissing', 'Target catalog is required for this governed event action.') }}</mat-hint>\n }\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventMapping', 'Mapping') }}</mat-label>\n <textarea\n matInput\n data-testid=\"chart-editor-event-pointClick-mapping\"\n rows=\"4\"\n [ngModel]=\"eventMappingText('pointClick')\"\n (ngModelChange)=\"setEventMapping('pointClick', $event)\"\n [disabled]=\"isReadonly() || !eventAction('pointClick')\"\n ></textarea>\n </mat-form-field>\n </mat-card-content>\n </mat-card>\n\n <mat-card class=\"editor-card\" data-testid=\"chart-editor-event-selectionChange\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.events.selectionChangeTitle', 'Selection change') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventAction', 'Action') }}</mat-label>\n <mat-select data-testid=\"chart-editor-event-selectionChange-action\" [ngModel]=\"eventAction('selectionChange')\" (ngModelChange)=\"setEventAction('selectionChange', $event)\" [disabled]=\"isReadonly()\">\n <mat-option value=\"\">{{ t('praxis.charts.editor.events.none', 'None') }}</mat-option>\n @for (action of eventActionOptions; track action) {\n <mat-option [value]=\"action\">\n {{ t('praxis.charts.editor.eventAction.' + action, action) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventTarget', 'Target') }}</mat-label>\n <mat-select data-testid=\"chart-editor-event-selectionChange-target\" [ngModel]=\"eventTarget('selectionChange')\" (ngModelChange)=\"setEventTarget('selectionChange', $event)\" [disabled]=\"isReadonly() || !eventAction('selectionChange') || targetCatalogUnavailable(eventAction('selectionChange'), 'selectionChange')\">\n @for (target of targetOptions(eventAction('selectionChange'), 'selectionChange'); track target.id) {\n <mat-option [value]=\"target.id\">{{ target.label }}</mat-option>\n }\n </mat-select>\n @if (targetCatalogUnavailable(eventAction('selectionChange'), 'selectionChange')) {\n <mat-hint>{{ t('praxis.charts.editor.catalog.targetMissing', 'Target catalog is required for this governed event action.') }}</mat-hint>\n }\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventMapping', 'Mapping') }}</mat-label>\n <textarea\n matInput\n data-testid=\"chart-editor-event-selectionChange-mapping\"\n rows=\"4\"\n [ngModel]=\"eventMappingText('selectionChange')\"\n (ngModelChange)=\"setEventMapping('selectionChange', $event)\"\n [disabled]=\"isReadonly() || !eventAction('selectionChange')\"\n ></textarea>\n </mat-form-field>\n </mat-card-content>\n </mat-card>\n\n <mat-card class=\"editor-card\" data-testid=\"chart-editor-event-drillDown\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.events.drillDownTitle', 'Drill down') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventAction', 'Action') }}</mat-label>\n <mat-select data-testid=\"chart-editor-event-drillDown-action\" [ngModel]=\"eventAction('drillDown')\" (ngModelChange)=\"setEventAction('drillDown', $event)\" [disabled]=\"isReadonly()\">\n <mat-option value=\"\">{{ t('praxis.charts.editor.events.none', 'None') }}</mat-option>\n @for (action of eventActionOptions; track action) {\n <mat-option [value]=\"action\">\n {{ t('praxis.charts.editor.eventAction.' + action, action) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventTarget', 'Target') }}</mat-label>\n <mat-select data-testid=\"chart-editor-event-drillDown-target\" [ngModel]=\"eventTarget('drillDown')\" (ngModelChange)=\"setEventTarget('drillDown', $event)\" [disabled]=\"isReadonly() || !eventAction('drillDown') || targetCatalogUnavailable(eventAction('drillDown'), 'drillDown')\">\n @for (target of targetOptions(eventAction('drillDown'), 'drillDown'); track target.id) {\n <mat-option [value]=\"target.id\">{{ target.label }}</mat-option>\n }\n </mat-select>\n @if (targetCatalogUnavailable(eventAction('drillDown'), 'drillDown')) {\n <mat-hint>{{ t('praxis.charts.editor.catalog.targetMissing', 'Target catalog is required for this governed event action.') }}</mat-hint>\n }\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventMapping', 'Mapping') }}</mat-label>\n <textarea\n matInput\n data-testid=\"chart-editor-event-drillDown-mapping\"\n rows=\"4\"\n [ngModel]=\"eventMappingText('drillDown')\"\n (ngModelChange)=\"setEventMapping('drillDown', $event)\"\n [disabled]=\"isReadonly() || !eventAction('drillDown')\"\n ></textarea>\n </mat-form-field>\n </mat-card-content>\n </mat-card>\n\n <mat-card class=\"editor-card\" data-testid=\"chart-editor-event-crossFilter\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.events.crossFilterTitle', 'Cross filter') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventAction', 'Action') }}</mat-label>\n <mat-select data-testid=\"chart-editor-event-crossFilter-action\" [ngModel]=\"eventAction('crossFilter')\" (ngModelChange)=\"setEventAction('crossFilter', $event)\" [disabled]=\"isReadonly()\">\n <mat-option value=\"\">{{ t('praxis.charts.editor.events.none', 'None') }}</mat-option>\n @for (action of eventActionOptions; track action) {\n <mat-option [value]=\"action\">\n {{ t('praxis.charts.editor.eventAction.' + action, action) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventTarget', 'Target') }}</mat-label>\n <mat-select data-testid=\"chart-editor-event-crossFilter-target\" [ngModel]=\"eventTarget('crossFilter')\" (ngModelChange)=\"setEventTarget('crossFilter', $event)\" [disabled]=\"isReadonly() || !eventAction('crossFilter') || targetCatalogUnavailable(eventAction('crossFilter'), 'crossFilter')\">\n @for (target of targetOptions(eventAction('crossFilter'), 'crossFilter'); track target.id) {\n <mat-option [value]=\"target.id\">{{ target.label }}</mat-option>\n }\n </mat-select>\n @if (targetCatalogUnavailable(eventAction('crossFilter'), 'crossFilter')) {\n <mat-hint>{{ t('praxis.charts.editor.catalog.targetMissing', 'Target catalog is required for this governed event action.') }}</mat-hint>\n }\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventMapping', 'Mapping') }}</mat-label>\n <textarea\n matInput\n data-testid=\"chart-editor-event-crossFilter-mapping\"\n rows=\"4\"\n [ngModel]=\"eventMappingText('crossFilter')\"\n (ngModelChange)=\"setEventMapping('crossFilter', $event)\"\n [disabled]=\"isReadonly() || !eventAction('crossFilter')\"\n ></textarea>\n </mat-form-field>\n </mat-card-content>\n </mat-card>\n </div>\n }\n\n @case ('preview') {\n <mat-card class=\"editor-card\">\n <mat-card-content>\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 </mat-card-content>\n </mat-card>\n }\n }\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\" [class.editor-issue--warning]=\"issue.severity === 'warning'\">\n <div class=\"editor-issue__header\">\n <strong>{{ issue.field }}</strong>\n @if (issueTargetSection(issue); as targetSection) {\n <button\n mat-button\n type=\"button\"\n class=\"editor-issue__jump\"\n [attr.data-testid]=\"'chart-editor-issue-jump-' + issue.code\"\n [attr.aria-label]=\"t('praxis.charts.editor.issues.jumpToSection', 'Go to {{ section }}', { section: sectionLabel(targetSection) })\"\n (click)=\"setSection(targetSection)\"\n >\n {{ sectionLabel(targetSection) }} \u2192\n </button>\n }\n </div>\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()) {\n <div\n class=\"editor-empty editor-empty--actionable editor-preview-degraded\"\n data-testid=\"chart-editor-preview-degraded-warning\"\n >\n <span>{{\n effectivePreview()\n ? t('praxis.charts.editor.preview.degradedNotice', 'Aesthetic simulation active with synthetic sample data while contract validation issues remain open.')\n : t('praxis.charts.editor.preview.invalid', 'Preview is unavailable while the contract has blocking errors.')\n }}</span>\n @if (firstInvalidSection(); as invalidSection) {\n <button\n mat-stroked-button\n type=\"button\"\n data-testid=\"chart-editor-preview-jump-invalid\"\n [attr.aria-label]=\"t('praxis.charts.editor.issues.jumpToSection', 'Go to {{ section }}', { section: sectionLabel(invalidSection) })\"\n (click)=\"setSection(invalidSection)\"\n >\n {{ sectionLabel(invalidSection) }} \u2192\n </button>\n }\n </div>\n }\n @if (effectivePreview(); 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 <p class=\"editor-caption\" data-testid=\"chart-editor-preview-origin\">\n {{\n doc().source.kind === 'praxis.stats'\n ? t('praxis.charts.editor.preview.originStatsSynthetic', 'Previewing synthetic sample for governed backend stats (praxis.stats).')\n : chartPreview.dataOrigin === 'local'\n ? t('praxis.charts.editor.preview.originLocal', 'Previewing widget local dataset (inputs.data).')\n : t('praxis.charts.editor.preview.originSynthetic', 'Previewing synthetic sample data (no local dataset records).')\n }}\n </p>\n <praxis-chart [config]=\"chartPreview.config\" [data]=\"chartPreview.data\"></praxis-chart>\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:16px;min-width:0}.editor-nav{display:flex;gap:6px;flex-wrap:wrap;padding:4px;border-radius:14px;background:var(--md-sys-color-surface-container-low, rgba(18, 99, 180, .04));border:1px solid color-mix(in srgb,var(--md-sys-color-outline-variant, var(--md-sys-color-outline, #c5c7ce)) 64%,transparent)}.editor-nav button{border-radius:10px;border-color:transparent;display:inline-flex;align-items:center;gap:6px}.editor-nav button.active{background:color-mix(in srgb,var(--pfx-settings-panel-primary, var(--md-sys-color-primary, #1263b4)) 22%,transparent);color:var(--pfx-settings-panel-primary, var(--md-sys-color-primary, #1263b4));border-color:color-mix(in srgb,var(--pfx-settings-panel-primary, var(--md-sys-color-primary, #1263b4)) 48%,transparent);font-weight:600}.editor-nav__badge{display:inline-flex;align-items:center;justify-content:center;min-width:18px;height:18px;padding:0 5px;margin-left:6px;border-radius:999px;font-size:.7rem;font-weight:700;line-height:1;background:color-mix(in srgb,var(--md-sys-color-tertiary, #f59e0b) 24%,transparent);color:var(--pfx-settings-panel-fg, var(--md-sys-color-on-surface, inherit))}.editor-nav__badge--error{background:color-mix(in srgb,var(--pfx-settings-panel-error, var(--md-sys-color-error, #b3261e)) 24%,transparent);color:var(--pfx-settings-panel-error, var(--md-sys-color-error, #b3261e))}:host ::ng-deep .mat-mdc-form-field-hint-wrapper,:host ::ng-deep .mat-mdc-form-field-error-wrapper{position:relative!important;top:auto!important;left:auto!important;right:auto!important;padding-top:4px;line-height:1.35}:host ::ng-deep .mat-mdc-form-field-bottom-align:before{display:none}.editor-layout{display:grid;gap:16px;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:16px;border:1px solid color-mix(in srgb,var(--md-sys-color-outline-variant, var(--md-sys-color-outline, #c5c7ce)) 72%,transparent);background:var(--md-sys-color-surface-container-low, rgba(18, 99, 180, .03))}.editor-grid{display:grid;gap:14px;grid-template-columns:repeat(auto-fit,minmax(210px,1fr));align-items:start}.editor-toggle-cluster{display:flex;flex-wrap:wrap;align-items:center;gap:14px 24px}.editor-stack{display:grid;gap:14px}.editor-state-group{padding-top:14px;border-top:1px solid color-mix(in srgb,var(--md-sys-color-outline-variant, var(--md-sys-color-outline, #c5c7ce)) 48%,transparent)}.editor-state-group:first-child{padding-top:0;border-top:0}.editor-palette-swatches{display:flex;flex-wrap:wrap;align-items:center;gap:8px}.editor-palette-swatch-chip{display:inline-flex;align-items:center;gap:6px;padding:4px 8px;border-radius:999px;border:1px solid color-mix(in srgb,var(--md-sys-color-outline-variant, var(--md-sys-color-outline, #c5c7ce)) 68%,transparent);background:color-mix(in srgb,var(--pfx-settings-panel-surface, var(--md-sys-color-surface-container, transparent)) 88%,transparent);font-size:.76rem}.editor-palette-swatch-dot{width:16px;height:16px;border-radius:50%;border:1px solid rgba(128,128,128,.35);flex-shrink:0}.editor-palette-color-input{width:22px;height:22px;padding:0;border:0;border-radius:50%;background:transparent;cursor:pointer}.editor-palette-swatch-code{font-family:var(--pdx-font-mono, ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace);font-size:.74rem;color:var(--md-sys-color-on-surface, inherit)}.editor-palette-swatch-remove{appearance:none;border:0;background:transparent;color:var(--md-sys-color-on-surface-variant, #5a5d67);font-size:.95rem;line-height:1;padding:0 2px;cursor:pointer}.editor-row-card{padding:14px;border-radius:14px;border:1px solid color-mix(in srgb,var(--md-sys-color-outline-variant, var(--md-sys-color-outline, #c5c7ce)) 54%,transparent);background:color-mix(in srgb,var(--pfx-settings-panel-surface, var(--md-sys-color-surface-container, transparent)) 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:12px;background:color-mix(in srgb,var(--md-sys-color-error, #b3261e) 10%,transparent);border:1px solid color-mix(in srgb,var(--md-sys-color-error, #b3261e) 24%,transparent)}.editor-issue--warning{background:color-mix(in srgb,var(--md-sys-color-tertiary, #f59e0b) 10%,transparent);border-color:color-mix(in srgb,var(--md-sys-color-tertiary, #f59e0b) 26%,transparent)}.editor-issue__header{display:flex;align-items:center;justify-content:space-between;gap:8px;flex-wrap:wrap;margin-bottom:4px}.editor-issue strong{display:block;font-size:.84rem}.editor-issue__jump{min-height:26px;padding:0 8px;font-size:.76rem}.editor-caption{margin:0 0 12px;color:var(--md-sys-color-on-surface-variant, #5a5d67);font-size:.88rem;line-height:1.45}.editor-empty{padding:16px;border-radius:12px;background:color-mix(in srgb,var(--md-sys-color-surface-variant, #eceff4) 52%,transparent);border:1px dashed color-mix(in srgb,var(--md-sys-color-outline-variant, var(--md-sys-color-outline, #c5c7ce)) 64%,transparent);color:var(--md-sys-color-on-surface-variant, #5a5d67);font-size:.88rem}.editor-empty--actionable{display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap}.editor-select-trigger{display:inline-flex;align-items:center;gap:8px;min-width:0;max-width:100%}.editor-select-trigger__icon{width:18px;height:18px;font-size:18px;flex-shrink:0;color:var(--md-sys-color-primary, #1263b4)}.editor-select-trigger__label{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-weight:500}.editor-select-trigger__code{flex-shrink:0;max-width:45%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-family:var(--pdx-font-mono, ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace);font-size:.72rem;padding:1px 6px;border-radius:6px;background:color-mix(in srgb,currentColor 8%,transparent);color:color-mix(in srgb,currentColor 72%,transparent)}::ng-deep .editor-select-search{position:sticky;top:0;z-index:2;display:flex;align-items:center;gap:8px;padding:8px 12px;background:var(--md-sys-color-surface-container-highest, #f4f6fa);border-bottom:1px solid color-mix(in srgb,var(--md-sys-color-outline, #c5c7ce) 45%,transparent)}::ng-deep .editor-select-search mat-icon{width:18px;height:18px;font-size:18px;color:var(--md-sys-color-on-surface-variant, #5a5d67);flex-shrink:0}::ng-deep .editor-select-search input{width:100%;border:0;outline:none;background:transparent;color:inherit;font:inherit;font-size:.85rem}::ng-deep .mat-mdc-option.editor-rich-mat-option{min-height:54px;height:auto;padding-block:8px;align-items:flex-start}::ng-deep .editor-rich-option{display:flex;align-items:flex-start;gap:10px;min-width:0;width:100%;line-height:1.25}::ng-deep .editor-rich-option__icon{width:28px;height:28px;border-radius:8px;background:color-mix(in srgb,var(--md-sys-color-primary, #1263b4) 12%,transparent);color:var(--md-sys-color-primary, #1263b4);display:inline-flex;align-items:center;justify-content:center;flex-shrink:0;margin-top:1px}::ng-deep .editor-rich-option__icon mat-icon{width:16px;height:16px;font-size:16px}::ng-deep .editor-rich-option__body{display:flex;flex-direction:column;gap:2px;min-width:0;flex:1 1 auto}::ng-deep .editor-rich-option__header{display:flex;align-items:center;gap:6px;flex-wrap:wrap;min-width:0}::ng-deep .editor-rich-option__label{font-weight:600;font-size:.88rem;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}::ng-deep .editor-rich-option__badge,.editor-resource-summary__badge{display:inline-flex;align-items:center;padding:1px 6px;border-radius:999px;font-size:.68rem;font-weight:600;background:color-mix(in srgb,currentColor 10%,transparent);color:color-mix(in srgb,currentColor 78%,transparent);white-space:nowrap}::ng-deep .editor-rich-option__code,.editor-resource-summary__code{font-family:var(--pdx-font-mono, ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace);font-size:.72rem;color:var(--md-sys-color-primary, #1263b4);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}::ng-deep .editor-rich-option__description,.editor-resource-summary__description{margin:0;font-size:.75rem;color:var(--md-sys-color-on-surface-variant, #5a5d67);display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;white-space:normal}.editor-resource-summary{display:flex;align-items:flex-start;gap:10px;padding:10px 12px;border-radius:12px;background:color-mix(in srgb,var(--md-sys-color-primary, #1263b4) 6%,transparent);border:1px solid color-mix(in srgb,var(--md-sys-color-primary, #1263b4) 18%,transparent)}.editor-resource-summary__icon{width:28px;height:28px;border-radius:8px;background:color-mix(in srgb,var(--md-sys-color-primary, #1263b4) 12%,transparent);color:var(--md-sys-color-primary, #1263b4);display:inline-flex;align-items:center;justify-content:center;flex-shrink:0;margin-top:1px}.editor-resource-summary__icon mat-icon{width:16px;height:16px;font-size:16px}.editor-resource-summary__body{display:grid;gap:2px;min-width:0;flex:1 1 auto}.editor-resource-summary__header{display:flex;align-items:center;gap:6px;flex-wrap:wrap}.editor-side praxis-chart{display:block;width:100%;min-width:0;max-width:100%}.editor-preview-degraded{margin-bottom:12px;background:color-mix(in srgb,var(--md-sys-color-tertiary, #f59e0b) 10%,transparent);border-style:solid;border-color:color-mix(in srgb,var(--md-sys-color-tertiary, #f59e0b) 32%,transparent)}@container praxis-chart-editor (min-width: 841px){.editor-nav__preview-btn:not(.active){display:none}}@container praxis-chart-editor (max-width: 840px){.editor-layout{grid-template-columns:minmax(0,1fr)}.editor-shell--preview-active .editor-form{display:none}}@media(max-width:960px){.editor-layout{grid-template-columns:minmax(0,1fr)}}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$3.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$3.NumberValueAccessor, selector: "input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]" }, { kind: "directive", type: i1$3.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$3.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i2.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: MatCardModule }, { kind: "component", type: i3.MatCard, selector: "mat-card", inputs: ["appearance"], exportAs: ["matCard"] }, { kind: "directive", type: i3.MatCardContent, selector: "mat-card-content" }, { kind: "component", type: i3.MatCardHeader, selector: "mat-card-header" }, { kind: "directive", type: i3.MatCardTitle, selector: "mat-card-title, [mat-card-title], [matCardTitle]" }, { kind: "ngmodule", type: MatFormFieldModule }, { kind: "component", type: i4.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i4.MatLabel, selector: "mat-label" }, { kind: "directive", type: i4.MatHint, selector: "mat-hint", inputs: ["align", "id"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i5.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatInputModule }, { kind: "directive", type: i6.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: i7.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: "directive", type: i7.MatSelectTrigger, selector: "mat-select-trigger" }, { kind: "component", type: i7.MatOption, selector: "mat-option", inputs: ["value", "id", "disabled"], outputs: ["onSelectionChange"], exportAs: ["matOption"] }, { kind: "ngmodule", type: MatSlideToggleModule }, { kind: "component", type: i8.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: "directive", type: PraxisIconDirective, selector: "mat-icon[praxisIcon]", inputs: ["praxisIcon"] }, { kind: "component", type: PraxisChartComponent, selector: "praxis-chart", inputs: ["config", "resourceKey", "data", "chartDocument", "filterCriteria", "queryContext", "remoteDataResolver", "enableCustomization", "availableResources", "availableFields", "availableTargets"], outputs: ["pointClick", "pointAction", "selectionChange", "drillDown", "crossFilter", "queryRequest", "loadStateChange", "chartDocumentApplied", "chartDocumentSaved", "chartDocumentPatchChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
14913
16609
|
}
|
|
14914
16610
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: PraxisChartConfigEditor, decorators: [{
|
|
14915
16611
|
type: Component,
|
|
@@ -14924,7 +16620,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
|
|
|
14924
16620
|
MatSlideToggleModule,
|
|
14925
16621
|
PraxisIconDirective,
|
|
14926
16622
|
PraxisChartComponent
|
|
14927
|
-
], providers: [providePraxisChartsI18n()], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"editor-shell\" [class.editor-shell--preview-active]=\"activeSection() === 'preview'\">\n <div class=\"editor-nav\" role=\"toolbar\" [attr.aria-label]=\"t('praxis.charts.editor.sectionsNav', 'Se\u00E7\u00F5es do editor')\">\n @for (section of sections; track section.id) {\n <button\n mat-stroked-button\n type=\"button\"\n [attr.id]=\"'chart-editor-tab-' + section.id\"\n aria-controls=\"chart-editor-tabpanel\"\n [attr.aria-pressed]=\"activeSection() === section.id\"\n [tabIndex]=\"activeSection() === section.id ? 0 : -1\"\n [attr.data-testid]=\"'chart-editor-section-' + section.id\"\n [class.active]=\"activeSection() === section.id\"\n [class.editor-nav__preview-btn]=\"section.id === 'preview'\"\n [class.editor-nav__has-error]=\"sectionHasError(section.id)\"\n (click)=\"setSection(section.id)\"\n (keydown)=\"onSectionNavKeydown($event, $index)\"\n >\n <span>{{ t(section.labelKey, section.fallback) }}</span>\n @if (sectionIssueCount(section.id) > 0) {\n <span\n class=\"editor-nav__badge\"\n aria-hidden=\"true\"\n [class.editor-nav__badge--error]=\"sectionHasError(section.id)\"\n [attr.data-testid]=\"'chart-editor-section-badge-' + section.id\"\n >\n {{ sectionIssueCount(section.id) }}\n </span>\n }\n </button>\n }\n </div>\n\n <div class=\"editor-layout\">\n <div\n class=\"editor-form\"\n id=\"chart-editor-tabpanel\"\n role=\"tabpanel\"\n [attr.aria-labelledby]=\"'chart-editor-tab-' + activeSection()\"\n >\n @switch (activeSection()) {\n @case ('general') {\n <mat-card class=\"editor-card\">\n <mat-card-content 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 </mat-card-content>\n </mat-card>\n }\n\n @case ('data') {\n <div class=\"editor-stack\">\n <mat-card class=\"editor-card\">\n <mat-card-content class=\"editor-stack\">\n <p class=\"editor-caption\" data-testid=\"chart-editor-effective-origin\">\n {{ effectiveDataOriginSummary() }}\n </p>\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\n data-testid=\"chart-editor-resource\"\n [ngModel]=\"resourceValue()\"\n (ngModelChange)=\"setResource($event)\"\n (openedChange)=\"setResourceSearchTerm('')\"\n [disabled]=\"isReadonly() || resourceCatalogUnavailable()\"\n >\n @if (selectedResourceOption(); as selectedRes) {\n <mat-select-trigger>\n <span class=\"editor-select-trigger\">\n <mat-icon class=\"editor-select-trigger__icon\" [praxisIcon]=\"selectedRes.icon || 'api'\"></mat-icon>\n <span class=\"editor-select-trigger__label\">{{ selectedRes.label }}</span>\n <span class=\"editor-select-trigger__code\">{{ selectedRes.path }}</span>\n </span>\n </mat-select-trigger>\n }\n <div class=\"editor-select-search\" (click)=\"$event.stopPropagation()\">\n <mat-icon aria-hidden=\"true\">search</mat-icon>\n <input\n type=\"search\"\n data-testid=\"chart-editor-resource-search\"\n [placeholder]=\"t('praxis.charts.editor.field.resourceSearchPlaceholder', 'Search by name, key, or endpoint...')\"\n [ngModel]=\"resourceSearchTerm()\"\n (ngModelChange)=\"setResourceSearchTerm($event)\"\n (keydown)=\"onSelectSearchKeydown($event)\"\n />\n </div>\n @for (resource of filteredResourceOptions(); track resource.id) {\n <mat-option [value]=\"resource.path\" class=\"editor-rich-mat-option\">\n <div class=\"editor-rich-option\">\n <span class=\"editor-rich-option__icon\" aria-hidden=\"true\">\n <mat-icon [praxisIcon]=\"resource.icon || 'api'\"></mat-icon>\n </span>\n <div class=\"editor-rich-option__body\">\n <div class=\"editor-rich-option__header\">\n <span class=\"editor-rich-option__label\">{{ resource.label }}</span>\n @if (resource.group) {\n <span class=\"editor-rich-option__badge\">{{ resource.group }}</span>\n }\n @if (resource.readOnly) {\n <span class=\"editor-rich-option__badge\">{{ t('praxis.charts.editor.catalog.readOnly', 'Read-only') }}</span>\n }\n </div>\n <span class=\"editor-rich-option__code\">\n {{ resource.path }}{{ resource.id && resource.id !== resource.path && resource.id !== resource.label ? ' \u00B7 ' + resource.id : '' }}\n </span>\n @if (resource.description) {\n <span class=\"editor-rich-option__description\">{{ resource.description }}</span>\n }\n </div>\n </div>\n </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() || isResolvingResource() || 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 (isResolvingResource()) {\n <mat-hint data-testid=\"chart-editor-resolving-resource\">\n {{ t('praxis.charts.editor.catalog.resolvingResource', 'Resolving governed stats operations and fields for the selected resource...') }}\n </mat-hint>\n } @else 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 (doc().source.kind === 'praxis.stats' && selectedResourceOption(); as selectedRes) {\n <div class=\"editor-resource-summary\" data-testid=\"chart-editor-resource-summary\">\n <span class=\"editor-resource-summary__icon\" aria-hidden=\"true\">\n <mat-icon [praxisIcon]=\"selectedRes.icon || 'api'\"></mat-icon>\n </span>\n <div class=\"editor-resource-summary__body\">\n <div class=\"editor-resource-summary__header\">\n <strong>{{ selectedRes.label }}</strong>\n @if (selectedRes.group) {\n <span class=\"editor-resource-summary__badge\">{{ selectedRes.group }}</span>\n }\n @if (selectedRes.readOnly) {\n <span class=\"editor-resource-summary__badge\">{{ t('praxis.charts.editor.catalog.readOnly', 'Read-only') }}</span>\n }\n </div>\n <span class=\"editor-resource-summary__code\">\n {{ selectedRes.path }}{{ selectedRes.id && selectedRes.id !== selectedRes.path && selectedRes.id !== selectedRes.label ? ' \u00B7 ' + selectedRes.id : '' }}\n </span>\n @if (selectedRes.description) {\n <p class=\"editor-resource-summary__description\">{{ selectedRes.description }}</p>\n }\n </div>\n </div>\n }\n </mat-card-content>\n </mat-card>\n\n @if (showTimeseriesControls()) {\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.specialization.timeseriesTitle', 'Timeseries options') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.granularity', 'Granularity') }}</mat-label>\n <mat-select [ngModel]=\"granularityValue()\" (ngModelChange)=\"setGranularity($event)\" [disabled]=\"isReadonly()\">\n @for (granularity of timeGranularities; track granularity) {\n <mat-option [value]=\"granularity\">\n {{ t('praxis.charts.editor.granularity.' + granularity, granularity) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n <mat-slide-toggle\n [ngModel]=\"fillGapsValue()\"\n (ngModelChange)=\"setFillGaps($event)\"\n [disabled]=\"isReadonly()\"\n >\n {{ t('praxis.charts.editor.field.fillGaps', 'Fill missing intervals') }}\n </mat-slide-toggle>\n </mat-card-content>\n </mat-card>\n }\n\n @if (showDistributionControls()) {\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.specialization.distributionTitle', 'Distribution options') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.distributionMode', 'Distribution mode') }}</mat-label>\n <mat-select data-testid=\"chart-editor-distribution-mode\" [ngModel]=\"distributionModeValue()\" (ngModelChange)=\"setDistributionMode($event)\" [disabled]=\"isReadonly()\">\n @for (mode of distributionModes; track mode) {\n <mat-option [value]=\"mode\">\n {{ t('praxis.charts.editor.distributionMode.' + mode, mode) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n @if (distributionModeValue() === 'histogram') {\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.bucketSize', 'Bucket size') }}</mat-label>\n <input matInput data-testid=\"chart-editor-bucket-size\" [ngModel]=\"bucketSizeValue()\" (ngModelChange)=\"setBucketSize($event)\" [disabled]=\"isReadonly()\" />\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.bucketCount', 'Bucket count') }}</mat-label>\n <input matInput data-testid=\"chart-editor-bucket-count\" [ngModel]=\"bucketCountValue()\" (ngModelChange)=\"setBucketCount($event)\" [disabled]=\"isReadonly()\" />\n </mat-form-field>\n }\n </mat-card-content>\n </mat-card>\n }\n\n @if (showComparisonControls()) {\n <mat-card class=\"editor-card\" data-testid=\"chart-editor-comparison-options\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.specialization.comparisonTitle', 'Comparison period') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.comparisonPeriodField', 'Period field') }}</mat-label>\n @if (fieldOptions('time').length) {\n <mat-select\n [ngModel]=\"comparisonPeriodFieldValue()\"\n (ngModelChange)=\"setComparisonPeriodField($event)\"\n (openedChange)=\"setFieldSearchTerm('')\"\n [disabled]=\"isReadonly()\"\n >\n <div class=\"editor-select-search\" (click)=\"$event.stopPropagation()\">\n <mat-icon aria-hidden=\"true\">search</mat-icon>\n <input\n type=\"search\"\n [placeholder]=\"t('praxis.charts.editor.field.fieldSearchPlaceholder', 'Search field...')\"\n [ngModel]=\"fieldSearchTerm()\"\n (ngModelChange)=\"setFieldSearchTerm($event)\"\n (keydown)=\"onSelectSearchKeydown($event)\"\n />\n </div>\n @for (field of filteredFieldOptions('time', comparisonPeriodFieldValue()); track field.field) {\n <mat-option [value]=\"field.field\" class=\"editor-rich-mat-option\">\n <div class=\"editor-rich-option\">\n <div class=\"editor-rich-option__body\">\n <div class=\"editor-rich-option__header\">\n <span class=\"editor-rich-option__label\">{{ field.label || field.field }}</span>\n @if (field.type) {\n <span class=\"editor-rich-option__badge\">{{ field.type }}</span>\n }\n </div>\n @if (field.label && field.label !== field.field) {\n <span class=\"editor-rich-option__code\">{{ field.field }}</span>\n }\n @if (field.description) {\n <span class=\"editor-rich-option__description\">{{ field.description }}</span>\n }\n </div>\n </div>\n </mat-option>\n }\n </mat-select>\n } @else {\n <mat-select [ngModel]=\"comparisonPeriodFieldValue()\" [disabled]=\"true\"></mat-select>\n }\n @if (!fieldOptions('time').length) {\n <mat-hint>{{ t('praxis.charts.editor.catalog.comparisonPeriodFieldsMissing', 'No governed time-series field is available for comparison periods.') }}</mat-hint>\n }\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.comparisonTimezone', 'Timezone') }}</mat-label>\n <input\n matInput\n [ngModel]=\"comparisonPeriodTimezoneValue()\"\n (ngModelChange)=\"setComparisonPeriodTimezone($event)\"\n [disabled]=\"isReadonly()\"\n />\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.comparisonPreset', 'Period preset') }}</mat-label>\n <mat-select\n [ngModel]=\"comparisonPeriodPresetValue()\"\n (ngModelChange)=\"setComparisonPeriodPreset($event)\"\n [disabled]=\"isReadonly()\"\n >\n @for (preset of comparisonPeriodPresets; track preset) {\n <mat-option [value]=\"preset\">\n {{ t('praxis.charts.editor.comparisonPreset.' + preset, preset) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.comparisonMode', 'Comparison mode') }}</mat-label>\n <mat-select\n [ngModel]=\"comparisonPeriodModeValue()\"\n (ngModelChange)=\"setComparisonPeriodMode($event)\"\n [disabled]=\"isReadonly()\"\n >\n @for (mode of comparisonPeriodModes; track mode) {\n <mat-option [value]=\"mode\">\n {{ t('praxis.charts.editor.comparisonMode.' + mode, mode) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n </mat-card-content>\n </mat-card>\n }\n\n @if (doc().source.kind === 'derived') {\n <mat-card class=\"editor-card\" data-testid=\"chart-editor-local-data\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.localData.title', 'Local dataset (inputs.data)') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-stack\">\n <p class=\"editor-caption\">\n {{ t('praxis.charts.editor.localData.caption', 'Edit the local records rendered when the data source is derived.') }}\n </p>\n\n @if (canConsolidateGaugeLocalData()) {\n <div class=\"editor-row-actions\">\n <button\n mat-stroked-button\n type=\"button\"\n data-testid=\"chart-local-data-consolidate-gauge\"\n (click)=\"consolidateGaugeLocalData()\"\n [disabled]=\"isReadonly()\"\n >\n {{ t('praxis.charts.editor.localData.consolidateGauge', 'Consolidate to 1 bucket for gauge') }}\n </button>\n </div>\n }\n\n @if (localDataRows().length) {\n @for (row of localDataRows(); track $index; let rowIndex = $index) {\n <div class=\"editor-row-card\" [attr.data-testid]=\"'chart-local-data-row-' + rowIndex\">\n <div class=\"editor-grid\">\n @for (dimField of localDataDimensionFields(); track dimField) {\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ dimField }}</mat-label>\n <input\n matInput\n [attr.data-testid]=\"'chart-local-data-dim-' + rowIndex + '-' + dimField\"\n [ngModel]=\"localDataCellStringValue(rowIndex, dimField)\"\n (ngModelChange)=\"setLocalDataCellValue(rowIndex, dimField, $event, 'dimension')\"\n [disabled]=\"isReadonly()\"\n />\n </mat-form-field>\n }\n @for (metricField of localDataMetricFields(); track metricField) {\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ metricField }}</mat-label>\n <input\n matInput\n type=\"number\"\n [attr.data-testid]=\"'chart-local-data-metric-' + rowIndex + '-' + metricField\"\n [ngModel]=\"localDataCellStringValue(rowIndex, metricField)\"\n (ngModelChange)=\"setLocalDataCellValue(rowIndex, metricField, $event, 'metric')\"\n [disabled]=\"isReadonly()\"\n />\n </mat-form-field>\n }\n </div>\n <div class=\"editor-row-actions\">\n <button\n mat-button\n type=\"button\"\n [attr.data-testid]=\"'chart-local-data-remove-row-' + rowIndex\"\n (click)=\"removeLocalDataRow(rowIndex)\"\n [disabled]=\"isReadonly()\"\n >\n {{ t('praxis.charts.editor.localData.removeRow', 'Remove record') }}\n </button>\n </div>\n </div>\n }\n } @else {\n <div class=\"editor-empty\" data-testid=\"chart-local-data-empty\">\n {{ t('praxis.charts.editor.localData.empty', 'No local records configured yet. Add a record to replace the synthetic preview sample.') }}\n </div>\n }\n\n <div>\n <button\n mat-stroked-button\n type=\"button\"\n data-testid=\"chart-local-data-add-row\"\n (click)=\"addLocalDataRow()\"\n [disabled]=\"isReadonly()\"\n >\n {{ t('praxis.charts.editor.localData.addRow', 'Add record') }}\n </button>\n </div>\n </mat-card-content>\n </mat-card>\n }\n </div>\n }\n\n @case ('motion') {\n <mat-card class=\"editor-card\">\n <mat-card-content class=\"editor-stack\">\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 <p class=\"editor-caption\" data-testid=\"chart-editor-motion-hint\">\n {{ t('praxis.charts.editor.motion.hint', 'Controls initial entrance timing and smooth transitions when filters or query context change, with automatic volume thresholds for high-density datasets.') }}\n </p>\n\n <div>\n <button\n mat-stroked-button\n type=\"button\"\n data-testid=\"chart-editor-simulate-motion\"\n (click)=\"simulateMotionTransition()\"\n [disabled]=\"normalizedDocument().motion?.enabled === false || !effectivePreview()\"\n >\n {{ t('praxis.charts.editor.motion.simulate', 'Simulate transition') }}\n </button>\n </div>\n </mat-card-content>\n </mat-card>\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-toggle-cluster\">\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 <div class=\"editor-grid\">\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 </div>\n\n @if (canApplyDomainSemanticPalette()) {\n <div class=\"editor-row-actions\">\n <button\n mat-stroked-button\n type=\"button\"\n data-testid=\"chart-editor-apply-domain-palette\"\n (click)=\"applyDomainSemanticPalette()\"\n [disabled]=\"isReadonly()\"\n [title]=\"t('praxis.charts.editor.appearance.applyDomainPaletteHint', 'Generates a custom palette with semantic colors (success, warning, danger, info) from the dimension categories.')\"\n >\n <mat-icon aria-hidden=\"true\">auto_fix_high</mat-icon>\n {{ t('praxis.charts.editor.appearance.applyDomainPalette', 'Apply domain semantic colors') }}\n </button>\n </div>\n }\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\n <div class=\"editor-palette-swatches\" data-testid=\"chart-editor-palette-swatches\">\n @for (swatch of tokenPaletteSwatches(); track $index) {\n <span class=\"editor-palette-swatch-chip\" [title]=\"swatch\">\n <span class=\"editor-palette-swatch-dot\" [style.background]=\"swatch\"></span>\n <code class=\"editor-palette-swatch-code\">{{ swatch }}</code>\n </span>\n }\n </div>\n } @else {\n <div class=\"editor-palette-swatches\" data-testid=\"chart-editor-palette-swatches\">\n @for (swatch of customPaletteColors(); track $index) {\n <span class=\"editor-palette-swatch-chip\" [title]=\"swatch\">\n <span class=\"editor-palette-swatch-dot\" [style.background]=\"swatch\"></span>\n <input\n type=\"color\"\n class=\"editor-palette-color-input\"\n [attr.data-testid]=\"'chart-editor-palette-color-' + $index\"\n [value]=\"swatchHexValue(swatch, $index)\"\n (input)=\"setCustomPaletteColorAt($index, $any($event.target).value)\"\n [disabled]=\"isReadonly()\"\n />\n <code class=\"editor-palette-swatch-code\">{{ swatch }}</code>\n <button\n type=\"button\"\n class=\"editor-palette-swatch-remove\"\n [attr.data-testid]=\"'chart-editor-palette-remove-' + $index\"\n (click)=\"removeCustomPaletteColor($index)\"\n [disabled]=\"isReadonly()\"\n [attr.aria-label]=\"t('praxis.charts.editor.appearance.removePaletteColor', 'Remove color')\"\n >\n \u00D7\n </button>\n </span>\n }\n <button\n mat-stroked-button\n type=\"button\"\n class=\"editor-palette-add-btn\"\n data-testid=\"chart-editor-add-palette-color\"\n (click)=\"addCustomPaletteColor()\"\n [disabled]=\"isReadonly()\"\n >\n + {{ t('praxis.charts.editor.appearance.addPaletteColor', 'Add color') }}\n </button>\n </div>\n\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=\"2\"\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-state-group\">\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=\"1\" [ngModel]=\"stateDescription('empty')\" (ngModelChange)=\"setStateDescription('empty', $event)\" [disabled]=\"isReadonly()\"></textarea>\n </mat-form-field>\n </div>\n </div>\n\n <div class=\"editor-state-group\">\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=\"1\" [ngModel]=\"stateDescription('loading')\" (ngModelChange)=\"setStateDescription('loading', $event)\" [disabled]=\"isReadonly()\"></textarea>\n </mat-form-field>\n </div>\n </div>\n\n <div class=\"editor-state-group\">\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=\"1\" [ngModel]=\"stateDescription('error')\" (ngModelChange)=\"setStateDescription('error', $event)\" [disabled]=\"isReadonly()\"></textarea>\n </mat-form-field>\n </div>\n </div>\n </mat-card-content>\n </mat-card>\n </div>\n }\n\n @case ('analytics') {\n <div class=\"editor-stack\">\n @if (showGaugePanel()) {\n <mat-card class=\"editor-card\" data-testid=\"chart-editor-gauge-scale\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.specialization.gaugeTitle', 'Gauge scale') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-stack\">\n <p class=\"editor-caption\">\n {{ t('praxis.charts.editor.specialization.gaugeHint', 'Gauge charts render exactly one bucket and one metric. Remote stats use group-by with limit 1; values outside the scale fail instead of being clamped.') }}\n </p>\n <div class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.gaugeMin', 'Scale minimum') }}</mat-label>\n <input\n matInput\n type=\"number\"\n data-testid=\"chart-editor-gauge-min\"\n [ngModel]=\"gaugeMinValue()\"\n (ngModelChange)=\"setGaugeMin($event)\"\n [disabled]=\"isReadonly()\"\n />\n </mat-form-field>\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.gaugeMax', 'Scale maximum') }}</mat-label>\n <input\n matInput\n type=\"number\"\n data-testid=\"chart-editor-gauge-max\"\n [ngModel]=\"gaugeMaxValue()\"\n (ngModelChange)=\"setGaugeMax($event)\"\n [disabled]=\"isReadonly()\"\n />\n </mat-form-field>\n </div>\n </mat-card-content>\n </mat-card>\n }\n\n @if (showComboPanel()) {\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.specialization.comboTitle', 'Combo guidance') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content>\n <p class=\"editor-caption\">\n {{ t('praxis.charts.editor.specialization.comboHint', 'Combo charts require at least two metrics and allow per-metric axis and series kind mapping.') }}\n </p>\n </mat-card-content>\n </mat-card>\n }\n\n @if (showPieDonutPanel()) {\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.specialization.pieDonutTitle', 'Composition guidance') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content>\n <p class=\"editor-caption\">\n {{ t('praxis.charts.editor.specialization.pieDonutHint', 'Pie, donut, funnel, pyramid and treemap charts keep one metric and use the first dimension as the category segment.') }}\n </p>\n </mat-card-content>\n </mat-card>\n }\n\n @if (showScatterPanel()) {\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.specialization.scatterTitle', 'Scatter guidance') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content>\n <p class=\"editor-caption\">\n {{ t('praxis.charts.editor.specialization.scatterHint', 'With one metric, scatter uses the first dimension as X and the metric as Y. With two metrics, the dimension identifies the grouped bucket and the metrics map to X and Y.') }}\n </p>\n </mat-card-content>\n </mat-card>\n }\n\n @if (canApplySemanticAnalyticsMapping()) {\n <div class=\"editor-row-actions\">\n <button\n mat-stroked-button\n type=\"button\"\n data-testid=\"chart-editor-smart-map-analytics\"\n (click)=\"applySemanticAnalyticsMapping()\"\n [disabled]=\"isReadonly()\"\n [title]=\"t('praxis.charts.editor.analytics.smartMapHint', 'Automatically selects the best dimension, metric, and aggregation from the governed field catalog.')\"\n >\n <mat-icon aria-hidden=\"true\">auto_fix_high</mat-icon>\n {{ t('praxis.charts.editor.analytics.smartMap', 'Suggest dimension & metric from schema') }}\n </button>\n </div>\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\n [ngModel]=\"dimension.field || ''\"\n (ngModelChange)=\"setDimensionField($index, $event)\"\n (openedChange)=\"setFieldSearchTerm('')\"\n [disabled]=\"isReadonly()\"\n >\n <div class=\"editor-select-search\" (click)=\"$event.stopPropagation()\">\n <mat-icon aria-hidden=\"true\">search</mat-icon>\n <input\n type=\"search\"\n [placeholder]=\"t('praxis.charts.editor.field.fieldSearchPlaceholder', 'Search field...')\"\n [ngModel]=\"fieldSearchTerm()\"\n (ngModelChange)=\"setFieldSearchTerm($event)\"\n (keydown)=\"onSelectSearchKeydown($event)\"\n />\n </div>\n @for (field of filteredFieldOptions('dimension', dimension.field); track field.field) {\n <mat-option [value]=\"field.field\" class=\"editor-rich-mat-option\">\n <div class=\"editor-rich-option\">\n <div class=\"editor-rich-option__body\">\n <div class=\"editor-rich-option__header\">\n <span class=\"editor-rich-option__label\">{{ field.label || field.field }}</span>\n @if (field.type) {\n <span class=\"editor-rich-option__badge\">{{ field.type }}</span>\n }\n </div>\n @if (field.label && field.label !== field.field) {\n <span class=\"editor-rich-option__code\">{{ field.field }}</span>\n }\n @if (field.description) {\n <span class=\"editor-rich-option__description\">{{ field.description }}</span>\n }\n </div>\n </div>\n </mat-option>\n }\n </mat-select>\n } @else {\n <mat-select [ngModel]=\"dimension.field || ''\" (ngModelChange)=\"setDimensionField($index, $event)\" [disabled]=\"true\"></mat-select>\n }\n @if (!fieldOptions('dimension').length) {\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\n [ngModel]=\"metric.field || ''\"\n (ngModelChange)=\"setMetricField($index, $event)\"\n (openedChange)=\"setFieldSearchTerm('')\"\n [disabled]=\"isReadonly()\"\n >\n <div class=\"editor-select-search\" (click)=\"$event.stopPropagation()\">\n <mat-icon aria-hidden=\"true\">search</mat-icon>\n <input\n type=\"search\"\n [placeholder]=\"t('praxis.charts.editor.field.fieldSearchPlaceholder', 'Search field...')\"\n [ngModel]=\"fieldSearchTerm()\"\n (ngModelChange)=\"setFieldSearchTerm($event)\"\n (keydown)=\"onSelectSearchKeydown($event)\"\n />\n </div>\n @for (field of filteredFieldOptions('metric', metric.field); track field.field) {\n <mat-option [value]=\"field.field\" class=\"editor-rich-mat-option\">\n <div class=\"editor-rich-option\">\n <div class=\"editor-rich-option__body\">\n <div class=\"editor-rich-option__header\">\n <span class=\"editor-rich-option__label\">{{ field.label || field.field }}</span>\n @if (field.type) {\n <span class=\"editor-rich-option__badge\">{{ field.type }}</span>\n }\n </div>\n @if (field.label && field.label !== field.field) {\n <span class=\"editor-rich-option__code\">{{ field.field }}</span>\n }\n @if (field.description) {\n <span class=\"editor-rich-option__description\">{{ field.description }}</span>\n }\n </div>\n </div>\n </mat-option>\n }\n </mat-select>\n } @else {\n <mat-select [ngModel]=\"metric.field || ''\" (ngModelChange)=\"setMetricField($index, $event)\" [disabled]=\"true\"></mat-select>\n }\n @if (!fieldOptions('metric').length) {\n <mat-hint>{{ t('praxis.charts.editor.catalog.metricFieldsMissing', 'No governed metrics are available for the current resource and operation.') }}</mat-hint>\n }\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\" [attr.data-testid]=\"'chart-editor-metric-label-' + $index\">\n <mat-label>{{ t('praxis.charts.editor.field.metricLabel', 'Metric label') }}</mat-label>\n <input matInput [ngModel]=\"metric.label || ''\" (ngModelChange)=\"setMetricLabel($index, $event)\" [disabled]=\"isReadonly()\" />\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\" [attr.data-testid]=\"'chart-editor-metric-aggregation-' + $index\">\n <mat-label>{{ t('praxis.charts.editor.field.metricAggregation', 'Aggregation') }}</mat-label>\n <mat-select [ngModel]=\"metric.aggregation || 'sum'\" (ngModelChange)=\"setMetricAggregation($index, $event)\" [disabled]=\"isReadonly()\">\n @for (aggregation of metricAggregationOptions(metric.field); track aggregation) {\n <mat-option [value]=\"aggregation\">\n {{ t('praxis.charts.editor.metricAggregation.' + aggregation, aggregation) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n @if (showMetricAxisControls()) {\n <mat-form-field class=\"editor-field\" appearance=\"outline\" [attr.data-testid]=\"'chart-editor-metric-axis-' + $index\">\n <mat-label>{{ t('praxis.charts.editor.field.metricAxis', 'Axis') }}</mat-label>\n <mat-select [ngModel]=\"metric.axis || 'primary'\" (ngModelChange)=\"setMetricAxis($index, $event)\" [disabled]=\"isReadonly()\">\n @for (axis of metricAxes; track axis) {\n <mat-option [value]=\"axis\">\n {{ t('praxis.charts.editor.metricAxis.' + axis, axis) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n }\n\n @if (showMetricSeriesKindControls()) {\n <mat-form-field class=\"editor-field\" appearance=\"outline\" [attr.data-testid]=\"'chart-editor-metric-series-kind-' + $index\">\n <mat-label>{{ t('praxis.charts.editor.field.metricSeriesKind', 'Series kind') }}</mat-label>\n <mat-select [ngModel]=\"metric.seriesKind || 'bar'\" (ngModelChange)=\"setMetricSeriesKind($index, $event)\" [disabled]=\"isReadonly()\">\n @for (seriesKind of metricSeriesKinds; track seriesKind) {\n <mat-option [value]=\"seriesKind\">\n {{ t('praxis.charts.editor.metricSeriesKind.' + seriesKind, seriesKind) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n }\n </div>\n\n <div class=\"editor-row-actions\">\n <button mat-button type=\"button\" [attr.data-testid]=\"'chart-editor-remove-metric-' + $index\" (click)=\"removeMetric($index)\" [disabled]=\"isReadonly() || metrics().length <= 1\">\n {{ t('praxis.charts.editor.analytics.removeMetric', 'Remove metric') }}\n </button>\n </div>\n </div>\n }\n\n <div>\n <button mat-stroked-button type=\"button\" data-testid=\"chart-editor-add-metric\" (click)=\"addMetric()\" [disabled]=\"isReadonly() || !canAddMetric()\">\n {{ t('praxis.charts.editor.analytics.addMetric', 'Add metric') }}\n </button>\n </div>\n </mat-card-content>\n </mat-card>\n </div>\n }\n\n @case ('events') {\n <div class=\"editor-stack\">\n <mat-card class=\"editor-card\" data-testid=\"chart-editor-event-pointClick\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.events.pointClickTitle', 'Point click') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventAction', 'Action') }}</mat-label>\n <mat-select data-testid=\"chart-editor-event-pointClick-action\" [ngModel]=\"eventAction('pointClick')\" (ngModelChange)=\"setEventAction('pointClick', $event)\" [disabled]=\"isReadonly()\">\n <mat-option value=\"\">{{ t('praxis.charts.editor.events.none', 'None') }}</mat-option>\n @for (action of eventActionOptions; track action) {\n <mat-option [value]=\"action\">\n {{ t('praxis.charts.editor.eventAction.' + action, action) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventTarget', 'Target') }}</mat-label>\n <mat-select data-testid=\"chart-editor-event-pointClick-target\" [ngModel]=\"eventTarget('pointClick')\" (ngModelChange)=\"setEventTarget('pointClick', $event)\" [disabled]=\"isReadonly() || !eventAction('pointClick') || targetCatalogUnavailable(eventAction('pointClick'), 'pointClick')\">\n @for (target of targetOptions(eventAction('pointClick'), 'pointClick'); track target.id) {\n <mat-option [value]=\"target.id\">{{ target.label }}</mat-option>\n }\n </mat-select>\n @if (targetCatalogUnavailable(eventAction('pointClick'), 'pointClick')) {\n <mat-hint>{{ t('praxis.charts.editor.catalog.targetMissing', 'Target catalog is required for this governed event action.') }}</mat-hint>\n }\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventMapping', 'Mapping') }}</mat-label>\n <textarea\n matInput\n data-testid=\"chart-editor-event-pointClick-mapping\"\n rows=\"4\"\n [ngModel]=\"eventMappingText('pointClick')\"\n (ngModelChange)=\"setEventMapping('pointClick', $event)\"\n [disabled]=\"isReadonly() || !eventAction('pointClick')\"\n ></textarea>\n </mat-form-field>\n </mat-card-content>\n </mat-card>\n\n <mat-card class=\"editor-card\" data-testid=\"chart-editor-event-selectionChange\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.events.selectionChangeTitle', 'Selection change') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventAction', 'Action') }}</mat-label>\n <mat-select data-testid=\"chart-editor-event-selectionChange-action\" [ngModel]=\"eventAction('selectionChange')\" (ngModelChange)=\"setEventAction('selectionChange', $event)\" [disabled]=\"isReadonly()\">\n <mat-option value=\"\">{{ t('praxis.charts.editor.events.none', 'None') }}</mat-option>\n @for (action of eventActionOptions; track action) {\n <mat-option [value]=\"action\">\n {{ t('praxis.charts.editor.eventAction.' + action, action) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventTarget', 'Target') }}</mat-label>\n <mat-select data-testid=\"chart-editor-event-selectionChange-target\" [ngModel]=\"eventTarget('selectionChange')\" (ngModelChange)=\"setEventTarget('selectionChange', $event)\" [disabled]=\"isReadonly() || !eventAction('selectionChange') || targetCatalogUnavailable(eventAction('selectionChange'), 'selectionChange')\">\n @for (target of targetOptions(eventAction('selectionChange'), 'selectionChange'); track target.id) {\n <mat-option [value]=\"target.id\">{{ target.label }}</mat-option>\n }\n </mat-select>\n @if (targetCatalogUnavailable(eventAction('selectionChange'), 'selectionChange')) {\n <mat-hint>{{ t('praxis.charts.editor.catalog.targetMissing', 'Target catalog is required for this governed event action.') }}</mat-hint>\n }\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventMapping', 'Mapping') }}</mat-label>\n <textarea\n matInput\n data-testid=\"chart-editor-event-selectionChange-mapping\"\n rows=\"4\"\n [ngModel]=\"eventMappingText('selectionChange')\"\n (ngModelChange)=\"setEventMapping('selectionChange', $event)\"\n [disabled]=\"isReadonly() || !eventAction('selectionChange')\"\n ></textarea>\n </mat-form-field>\n </mat-card-content>\n </mat-card>\n\n <mat-card class=\"editor-card\" data-testid=\"chart-editor-event-drillDown\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.events.drillDownTitle', 'Drill down') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventAction', 'Action') }}</mat-label>\n <mat-select data-testid=\"chart-editor-event-drillDown-action\" [ngModel]=\"eventAction('drillDown')\" (ngModelChange)=\"setEventAction('drillDown', $event)\" [disabled]=\"isReadonly()\">\n <mat-option value=\"\">{{ t('praxis.charts.editor.events.none', 'None') }}</mat-option>\n @for (action of eventActionOptions; track action) {\n <mat-option [value]=\"action\">\n {{ t('praxis.charts.editor.eventAction.' + action, action) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventTarget', 'Target') }}</mat-label>\n <mat-select data-testid=\"chart-editor-event-drillDown-target\" [ngModel]=\"eventTarget('drillDown')\" (ngModelChange)=\"setEventTarget('drillDown', $event)\" [disabled]=\"isReadonly() || !eventAction('drillDown') || targetCatalogUnavailable(eventAction('drillDown'), 'drillDown')\">\n @for (target of targetOptions(eventAction('drillDown'), 'drillDown'); track target.id) {\n <mat-option [value]=\"target.id\">{{ target.label }}</mat-option>\n }\n </mat-select>\n @if (targetCatalogUnavailable(eventAction('drillDown'), 'drillDown')) {\n <mat-hint>{{ t('praxis.charts.editor.catalog.targetMissing', 'Target catalog is required for this governed event action.') }}</mat-hint>\n }\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventMapping', 'Mapping') }}</mat-label>\n <textarea\n matInput\n data-testid=\"chart-editor-event-drillDown-mapping\"\n rows=\"4\"\n [ngModel]=\"eventMappingText('drillDown')\"\n (ngModelChange)=\"setEventMapping('drillDown', $event)\"\n [disabled]=\"isReadonly() || !eventAction('drillDown')\"\n ></textarea>\n </mat-form-field>\n </mat-card-content>\n </mat-card>\n\n <mat-card class=\"editor-card\" data-testid=\"chart-editor-event-crossFilter\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.events.crossFilterTitle', 'Cross filter') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventAction', 'Action') }}</mat-label>\n <mat-select data-testid=\"chart-editor-event-crossFilter-action\" [ngModel]=\"eventAction('crossFilter')\" (ngModelChange)=\"setEventAction('crossFilter', $event)\" [disabled]=\"isReadonly()\">\n <mat-option value=\"\">{{ t('praxis.charts.editor.events.none', 'None') }}</mat-option>\n @for (action of eventActionOptions; track action) {\n <mat-option [value]=\"action\">\n {{ t('praxis.charts.editor.eventAction.' + action, action) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventTarget', 'Target') }}</mat-label>\n <mat-select data-testid=\"chart-editor-event-crossFilter-target\" [ngModel]=\"eventTarget('crossFilter')\" (ngModelChange)=\"setEventTarget('crossFilter', $event)\" [disabled]=\"isReadonly() || !eventAction('crossFilter') || targetCatalogUnavailable(eventAction('crossFilter'), 'crossFilter')\">\n @for (target of targetOptions(eventAction('crossFilter'), 'crossFilter'); track target.id) {\n <mat-option [value]=\"target.id\">{{ target.label }}</mat-option>\n }\n </mat-select>\n @if (targetCatalogUnavailable(eventAction('crossFilter'), 'crossFilter')) {\n <mat-hint>{{ t('praxis.charts.editor.catalog.targetMissing', 'Target catalog is required for this governed event action.') }}</mat-hint>\n }\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventMapping', 'Mapping') }}</mat-label>\n <textarea\n matInput\n data-testid=\"chart-editor-event-crossFilter-mapping\"\n rows=\"4\"\n [ngModel]=\"eventMappingText('crossFilter')\"\n (ngModelChange)=\"setEventMapping('crossFilter', $event)\"\n [disabled]=\"isReadonly() || !eventAction('crossFilter')\"\n ></textarea>\n </mat-form-field>\n </mat-card-content>\n </mat-card>\n </div>\n }\n\n @case ('preview') {\n <mat-card class=\"editor-card\">\n <mat-card-content>\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 </mat-card-content>\n </mat-card>\n }\n }\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\" [class.editor-issue--warning]=\"issue.severity === 'warning'\">\n <div class=\"editor-issue__header\">\n <strong>{{ issue.field }}</strong>\n @if (issueTargetSection(issue); as targetSection) {\n <button\n mat-button\n type=\"button\"\n class=\"editor-issue__jump\"\n [attr.data-testid]=\"'chart-editor-issue-jump-' + issue.code\"\n [attr.aria-label]=\"t('praxis.charts.editor.issues.jumpToSection', 'Go to {{ section }}', { section: sectionLabel(targetSection) })\"\n (click)=\"setSection(targetSection)\"\n >\n {{ sectionLabel(targetSection) }} \u2192\n </button>\n }\n </div>\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()) {\n <div\n class=\"editor-empty editor-empty--actionable editor-preview-degraded\"\n data-testid=\"chart-editor-preview-degraded-warning\"\n >\n <span>{{\n effectivePreview()\n ? t('praxis.charts.editor.preview.degradedNotice', 'Aesthetic simulation active with synthetic sample data while contract validation issues remain open.')\n : t('praxis.charts.editor.preview.invalid', 'Preview is unavailable while the contract has blocking errors.')\n }}</span>\n @if (firstInvalidSection(); as invalidSection) {\n <button\n mat-stroked-button\n type=\"button\"\n data-testid=\"chart-editor-preview-jump-invalid\"\n [attr.aria-label]=\"t('praxis.charts.editor.issues.jumpToSection', 'Go to {{ section }}', { section: sectionLabel(invalidSection) })\"\n (click)=\"setSection(invalidSection)\"\n >\n {{ sectionLabel(invalidSection) }} \u2192\n </button>\n }\n </div>\n }\n @if (effectivePreview(); 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 <p class=\"editor-caption\" data-testid=\"chart-editor-preview-origin\">\n {{\n doc().source.kind === 'praxis.stats'\n ? t('praxis.charts.editor.preview.originStatsSynthetic', 'Previewing synthetic sample for governed backend stats (praxis.stats).')\n : chartPreview.dataOrigin === 'local'\n ? t('praxis.charts.editor.preview.originLocal', 'Previewing widget local dataset (inputs.data).')\n : t('praxis.charts.editor.preview.originSynthetic', 'Previewing synthetic sample data (no local dataset records).')\n }}\n </p>\n <praxis-chart [config]=\"chartPreview.config\" [data]=\"chartPreview.data\"></praxis-chart>\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:16px;min-width:0}.editor-nav{display:flex;gap:6px;flex-wrap:wrap;padding:4px;border-radius:14px;background:var(--md-sys-color-surface-container-low, rgba(18, 99, 180, .04));border:1px solid color-mix(in srgb,var(--md-sys-color-outline-variant, var(--md-sys-color-outline, #c5c7ce)) 64%,transparent)}.editor-nav button{border-radius:10px;border-color:transparent;display:inline-flex;align-items:center;gap:6px}.editor-nav button.active{background:color-mix(in srgb,var(--pfx-settings-panel-primary, var(--md-sys-color-primary, #1263b4)) 22%,transparent);color:var(--pfx-settings-panel-primary, var(--md-sys-color-primary, #1263b4));border-color:color-mix(in srgb,var(--pfx-settings-panel-primary, var(--md-sys-color-primary, #1263b4)) 48%,transparent);font-weight:600}.editor-nav__badge{display:inline-flex;align-items:center;justify-content:center;min-width:18px;height:18px;padding:0 5px;margin-left:6px;border-radius:999px;font-size:.7rem;font-weight:700;line-height:1;background:color-mix(in srgb,var(--md-sys-color-tertiary, #f59e0b) 24%,transparent);color:var(--pfx-settings-panel-fg, var(--md-sys-color-on-surface, inherit))}.editor-nav__badge--error{background:color-mix(in srgb,var(--pfx-settings-panel-error, var(--md-sys-color-error, #b3261e)) 24%,transparent);color:var(--pfx-settings-panel-error, var(--md-sys-color-error, #b3261e))}:host ::ng-deep .mat-mdc-form-field-hint-wrapper,:host ::ng-deep .mat-mdc-form-field-error-wrapper{position:relative!important;top:auto!important;left:auto!important;right:auto!important;padding-top:4px;line-height:1.35}:host ::ng-deep .mat-mdc-form-field-bottom-align:before{display:none}.editor-layout{display:grid;gap:16px;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:16px;border:1px solid color-mix(in srgb,var(--md-sys-color-outline-variant, var(--md-sys-color-outline, #c5c7ce)) 72%,transparent);background:var(--md-sys-color-surface-container-low, rgba(18, 99, 180, .03))}.editor-grid{display:grid;gap:14px;grid-template-columns:repeat(auto-fit,minmax(210px,1fr));align-items:start}.editor-toggle-cluster{display:flex;flex-wrap:wrap;align-items:center;gap:14px 24px}.editor-stack{display:grid;gap:14px}.editor-state-group{padding-top:14px;border-top:1px solid color-mix(in srgb,var(--md-sys-color-outline-variant, var(--md-sys-color-outline, #c5c7ce)) 48%,transparent)}.editor-state-group:first-child{padding-top:0;border-top:0}.editor-palette-swatches{display:flex;flex-wrap:wrap;align-items:center;gap:8px}.editor-palette-swatch-chip{display:inline-flex;align-items:center;gap:6px;padding:4px 8px;border-radius:999px;border:1px solid color-mix(in srgb,var(--md-sys-color-outline-variant, var(--md-sys-color-outline, #c5c7ce)) 68%,transparent);background:color-mix(in srgb,var(--pfx-settings-panel-surface, var(--md-sys-color-surface-container, transparent)) 88%,transparent);font-size:.76rem}.editor-palette-swatch-dot{width:16px;height:16px;border-radius:50%;border:1px solid rgba(128,128,128,.35);flex-shrink:0}.editor-palette-color-input{width:22px;height:22px;padding:0;border:0;border-radius:50%;background:transparent;cursor:pointer}.editor-palette-swatch-code{font-family:var(--pdx-font-mono, ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace);font-size:.74rem;color:var(--md-sys-color-on-surface, inherit)}.editor-palette-swatch-remove{appearance:none;border:0;background:transparent;color:var(--md-sys-color-on-surface-variant, #5a5d67);font-size:.95rem;line-height:1;padding:0 2px;cursor:pointer}.editor-row-card{padding:14px;border-radius:14px;border:1px solid color-mix(in srgb,var(--md-sys-color-outline-variant, var(--md-sys-color-outline, #c5c7ce)) 54%,transparent);background:color-mix(in srgb,var(--pfx-settings-panel-surface, var(--md-sys-color-surface-container, transparent)) 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:12px;background:color-mix(in srgb,var(--md-sys-color-error, #b3261e) 10%,transparent);border:1px solid color-mix(in srgb,var(--md-sys-color-error, #b3261e) 24%,transparent)}.editor-issue--warning{background:color-mix(in srgb,var(--md-sys-color-tertiary, #f59e0b) 10%,transparent);border-color:color-mix(in srgb,var(--md-sys-color-tertiary, #f59e0b) 26%,transparent)}.editor-issue__header{display:flex;align-items:center;justify-content:space-between;gap:8px;flex-wrap:wrap;margin-bottom:4px}.editor-issue strong{display:block;font-size:.84rem}.editor-issue__jump{min-height:26px;padding:0 8px;font-size:.76rem}.editor-caption{margin:0 0 12px;color:var(--md-sys-color-on-surface-variant, #5a5d67);font-size:.88rem;line-height:1.45}.editor-empty{padding:16px;border-radius:12px;background:color-mix(in srgb,var(--md-sys-color-surface-variant, #eceff4) 52%,transparent);border:1px dashed color-mix(in srgb,var(--md-sys-color-outline-variant, var(--md-sys-color-outline, #c5c7ce)) 64%,transparent);color:var(--md-sys-color-on-surface-variant, #5a5d67);font-size:.88rem}.editor-empty--actionable{display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap}.editor-select-trigger{display:inline-flex;align-items:center;gap:8px;min-width:0;max-width:100%}.editor-select-trigger__icon{width:18px;height:18px;font-size:18px;flex-shrink:0;color:var(--md-sys-color-primary, #1263b4)}.editor-select-trigger__label{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-weight:500}.editor-select-trigger__code{flex-shrink:0;max-width:45%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-family:var(--pdx-font-mono, ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace);font-size:.72rem;padding:1px 6px;border-radius:6px;background:color-mix(in srgb,currentColor 8%,transparent);color:color-mix(in srgb,currentColor 72%,transparent)}::ng-deep .editor-select-search{position:sticky;top:0;z-index:2;display:flex;align-items:center;gap:8px;padding:8px 12px;background:var(--md-sys-color-surface-container-highest, #f4f6fa);border-bottom:1px solid color-mix(in srgb,var(--md-sys-color-outline, #c5c7ce) 45%,transparent)}::ng-deep .editor-select-search mat-icon{width:18px;height:18px;font-size:18px;color:var(--md-sys-color-on-surface-variant, #5a5d67);flex-shrink:0}::ng-deep .editor-select-search input{width:100%;border:0;outline:none;background:transparent;color:inherit;font:inherit;font-size:.85rem}::ng-deep .mat-mdc-option.editor-rich-mat-option{min-height:54px;height:auto;padding-block:8px;align-items:flex-start}::ng-deep .editor-rich-option{display:flex;align-items:flex-start;gap:10px;min-width:0;width:100%;line-height:1.25}::ng-deep .editor-rich-option__icon{width:28px;height:28px;border-radius:8px;background:color-mix(in srgb,var(--md-sys-color-primary, #1263b4) 12%,transparent);color:var(--md-sys-color-primary, #1263b4);display:inline-flex;align-items:center;justify-content:center;flex-shrink:0;margin-top:1px}::ng-deep .editor-rich-option__icon mat-icon{width:16px;height:16px;font-size:16px}::ng-deep .editor-rich-option__body{display:flex;flex-direction:column;gap:2px;min-width:0;flex:1 1 auto}::ng-deep .editor-rich-option__header{display:flex;align-items:center;gap:6px;flex-wrap:wrap;min-width:0}::ng-deep .editor-rich-option__label{font-weight:600;font-size:.88rem;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}::ng-deep .editor-rich-option__badge,.editor-resource-summary__badge{display:inline-flex;align-items:center;padding:1px 6px;border-radius:999px;font-size:.68rem;font-weight:600;background:color-mix(in srgb,currentColor 10%,transparent);color:color-mix(in srgb,currentColor 78%,transparent);white-space:nowrap}::ng-deep .editor-rich-option__code,.editor-resource-summary__code{font-family:var(--pdx-font-mono, ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace);font-size:.72rem;color:var(--md-sys-color-primary, #1263b4);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}::ng-deep .editor-rich-option__description,.editor-resource-summary__description{margin:0;font-size:.75rem;color:var(--md-sys-color-on-surface-variant, #5a5d67);display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;white-space:normal}.editor-resource-summary{display:flex;align-items:flex-start;gap:10px;padding:10px 12px;border-radius:12px;background:color-mix(in srgb,var(--md-sys-color-primary, #1263b4) 6%,transparent);border:1px solid color-mix(in srgb,var(--md-sys-color-primary, #1263b4) 18%,transparent)}.editor-resource-summary__icon{width:28px;height:28px;border-radius:8px;background:color-mix(in srgb,var(--md-sys-color-primary, #1263b4) 12%,transparent);color:var(--md-sys-color-primary, #1263b4);display:inline-flex;align-items:center;justify-content:center;flex-shrink:0;margin-top:1px}.editor-resource-summary__icon mat-icon{width:16px;height:16px;font-size:16px}.editor-resource-summary__body{display:grid;gap:2px;min-width:0;flex:1 1 auto}.editor-resource-summary__header{display:flex;align-items:center;gap:6px;flex-wrap:wrap}.editor-side praxis-chart{display:block;width:100%;min-width:0;max-width:100%}.editor-preview-degraded{margin-bottom:12px;background:color-mix(in srgb,var(--md-sys-color-tertiary, #f59e0b) 10%,transparent);border-style:solid;border-color:color-mix(in srgb,var(--md-sys-color-tertiary, #f59e0b) 32%,transparent)}@container praxis-chart-editor (min-width: 841px){.editor-nav__preview-btn:not(.active){display:none}}@container praxis-chart-editor (max-width: 840px){.editor-layout{grid-template-columns:minmax(0,1fr)}.editor-shell--preview-active .editor-form{display:none}}@media(max-width:960px){.editor-layout{grid-template-columns:minmax(0,1fr)}}\n"] }]
|
|
16623
|
+
], providers: [providePraxisChartsI18n()], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"editor-shell\" [class.editor-shell--preview-active]=\"activeSection() === 'preview'\">\n <div class=\"editor-nav\" role=\"toolbar\" [attr.aria-label]=\"t('praxis.charts.editor.sectionsNav', 'Se\u00E7\u00F5es do editor')\">\n @for (section of sections; track section.id) {\n <button\n mat-stroked-button\n type=\"button\"\n [attr.id]=\"'chart-editor-tab-' + section.id\"\n aria-controls=\"chart-editor-tabpanel\"\n [attr.aria-pressed]=\"activeSection() === section.id\"\n [tabIndex]=\"activeSection() === section.id ? 0 : -1\"\n [attr.data-testid]=\"'chart-editor-section-' + section.id\"\n [class.active]=\"activeSection() === section.id\"\n [class.editor-nav__preview-btn]=\"section.id === 'preview'\"\n [class.editor-nav__has-error]=\"sectionHasError(section.id)\"\n (click)=\"setSection(section.id)\"\n (keydown)=\"onSectionNavKeydown($event, $index)\"\n >\n <span>{{ t(section.labelKey, section.fallback) }}</span>\n @if (sectionIssueCount(section.id) > 0) {\n <span\n class=\"editor-nav__badge\"\n aria-hidden=\"true\"\n [class.editor-nav__badge--error]=\"sectionHasError(section.id)\"\n [attr.data-testid]=\"'chart-editor-section-badge-' + section.id\"\n >\n {{ sectionIssueCount(section.id) }}\n </span>\n }\n </button>\n }\n </div>\n\n <div class=\"editor-layout\">\n <div\n class=\"editor-form\"\n id=\"chart-editor-tabpanel\"\n role=\"tabpanel\"\n [attr.aria-labelledby]=\"'chart-editor-tab-' + activeSection()\"\n >\n @switch (activeSection()) {\n @case ('general') {\n <mat-card class=\"editor-card\">\n <mat-card-content 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 </mat-card-content>\n </mat-card>\n }\n\n @case ('data') {\n <div class=\"editor-stack\">\n <mat-card class=\"editor-card\">\n <mat-card-content class=\"editor-stack\">\n <p class=\"editor-caption\" data-testid=\"chart-editor-effective-origin\">\n {{ effectiveDataOriginSummary() }}\n </p>\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\n data-testid=\"chart-editor-resource\"\n [ngModel]=\"resourceValue()\"\n (ngModelChange)=\"setResource($event)\"\n (openedChange)=\"setResourceSearchTerm('')\"\n [disabled]=\"isReadonly() || resourceCatalogUnavailable()\"\n >\n @if (selectedResourceOption(); as selectedRes) {\n <mat-select-trigger>\n <span class=\"editor-select-trigger\">\n <mat-icon class=\"editor-select-trigger__icon\" [praxisIcon]=\"selectedRes.icon || 'api'\"></mat-icon>\n <span class=\"editor-select-trigger__label\">{{ selectedRes.label }}</span>\n <span class=\"editor-select-trigger__code\">{{ selectedRes.path }}</span>\n </span>\n </mat-select-trigger>\n }\n <div class=\"editor-select-search\" (click)=\"$event.stopPropagation()\">\n <mat-icon aria-hidden=\"true\">search</mat-icon>\n <input\n type=\"search\"\n data-testid=\"chart-editor-resource-search\"\n [placeholder]=\"t('praxis.charts.editor.field.resourceSearchPlaceholder', 'Search by name, key, or endpoint...')\"\n [ngModel]=\"resourceSearchTerm()\"\n (ngModelChange)=\"setResourceSearchTerm($event)\"\n (keydown)=\"onSelectSearchKeydown($event)\"\n />\n </div>\n @for (resource of filteredResourceOptions(); track resource.id) {\n <mat-option [value]=\"resource.path\" class=\"editor-rich-mat-option\">\n <div class=\"editor-rich-option\">\n <span class=\"editor-rich-option__icon\" aria-hidden=\"true\">\n <mat-icon [praxisIcon]=\"resource.icon || 'api'\"></mat-icon>\n </span>\n <div class=\"editor-rich-option__body\">\n <div class=\"editor-rich-option__header\">\n <span class=\"editor-rich-option__label\">{{ resource.label }}</span>\n @if (resource.group) {\n <span class=\"editor-rich-option__badge\">{{ resource.group }}</span>\n }\n @if (resource.readOnly) {\n <span class=\"editor-rich-option__badge\">{{ t('praxis.charts.editor.catalog.readOnly', 'Read-only') }}</span>\n }\n </div>\n <span class=\"editor-rich-option__code\">\n {{ resource.path }}{{ resource.id && resource.id !== resource.path && resource.id !== resource.label ? ' \u00B7 ' + resource.id : '' }}\n </span>\n @if (resource.description) {\n <span class=\"editor-rich-option__description\">{{ resource.description }}</span>\n }\n </div>\n </div>\n </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() || isResolvingResource() || 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 (isResolvingResource()) {\n <mat-hint data-testid=\"chart-editor-resolving-resource\">\n {{ t('praxis.charts.editor.catalog.resolvingResource', 'Resolving governed stats operations and fields for the selected resource...') }}\n </mat-hint>\n } @else 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 (doc().source.kind === 'praxis.stats' && selectedResourceOption(); as selectedRes) {\n <div class=\"editor-resource-summary\" data-testid=\"chart-editor-resource-summary\">\n <span class=\"editor-resource-summary__icon\" aria-hidden=\"true\">\n <mat-icon [praxisIcon]=\"selectedRes.icon || 'api'\"></mat-icon>\n </span>\n <div class=\"editor-resource-summary__body\">\n <div class=\"editor-resource-summary__header\">\n <strong>{{ selectedRes.label }}</strong>\n @if (selectedRes.group) {\n <span class=\"editor-resource-summary__badge\">{{ selectedRes.group }}</span>\n }\n @if (selectedRes.readOnly) {\n <span class=\"editor-resource-summary__badge\">{{ t('praxis.charts.editor.catalog.readOnly', 'Read-only') }}</span>\n }\n </div>\n <span class=\"editor-resource-summary__code\">\n {{ selectedRes.path }}{{ selectedRes.id && selectedRes.id !== selectedRes.path && selectedRes.id !== selectedRes.label ? ' \u00B7 ' + selectedRes.id : '' }}\n </span>\n @if (selectedRes.description) {\n <p class=\"editor-resource-summary__description\">{{ selectedRes.description }}</p>\n }\n </div>\n </div>\n }\n </mat-card-content>\n </mat-card>\n\n @if (showTimeseriesControls()) {\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.specialization.timeseriesTitle', 'Timeseries options') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.granularity', 'Granularity') }}</mat-label>\n <mat-select [ngModel]=\"granularityValue()\" (ngModelChange)=\"setGranularity($event)\" [disabled]=\"isReadonly()\">\n @for (granularity of timeGranularities; track granularity) {\n <mat-option [value]=\"granularity\">\n {{ t('praxis.charts.editor.granularity.' + granularity, granularity) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n <mat-slide-toggle\n [ngModel]=\"fillGapsValue()\"\n (ngModelChange)=\"setFillGaps($event)\"\n [disabled]=\"isReadonly()\"\n >\n {{ t('praxis.charts.editor.field.fillGaps', 'Fill missing intervals') }}\n </mat-slide-toggle>\n </mat-card-content>\n </mat-card>\n }\n\n @if (showDistributionControls()) {\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.specialization.distributionTitle', 'Distribution options') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.distributionMode', 'Distribution mode') }}</mat-label>\n <mat-select data-testid=\"chart-editor-distribution-mode\" [ngModel]=\"distributionModeValue()\" (ngModelChange)=\"setDistributionMode($event)\" [disabled]=\"isReadonly()\">\n @for (mode of distributionModes; track mode) {\n <mat-option [value]=\"mode\">\n {{ t('praxis.charts.editor.distributionMode.' + mode, mode) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n @if (distributionModeValue() === 'histogram') {\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.bucketSize', 'Bucket size') }}</mat-label>\n <input matInput data-testid=\"chart-editor-bucket-size\" [ngModel]=\"bucketSizeValue()\" (ngModelChange)=\"setBucketSize($event)\" [disabled]=\"isReadonly()\" />\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.bucketCount', 'Bucket count') }}</mat-label>\n <input matInput data-testid=\"chart-editor-bucket-count\" [ngModel]=\"bucketCountValue()\" (ngModelChange)=\"setBucketCount($event)\" [disabled]=\"isReadonly()\" />\n </mat-form-field>\n }\n </mat-card-content>\n </mat-card>\n }\n\n @if (showComparisonControls()) {\n <mat-card class=\"editor-card\" data-testid=\"chart-editor-comparison-options\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.specialization.comparisonTitle', 'Comparison period') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.comparisonPeriodField', 'Period field') }}</mat-label>\n @if (fieldOptions('time').length) {\n <mat-select\n [ngModel]=\"comparisonPeriodFieldValue()\"\n (ngModelChange)=\"setComparisonPeriodField($event)\"\n (openedChange)=\"setFieldSearchTerm('')\"\n [disabled]=\"isReadonly()\"\n >\n <div class=\"editor-select-search\" (click)=\"$event.stopPropagation()\">\n <mat-icon aria-hidden=\"true\">search</mat-icon>\n <input\n type=\"search\"\n [placeholder]=\"t('praxis.charts.editor.field.fieldSearchPlaceholder', 'Search field...')\"\n [ngModel]=\"fieldSearchTerm()\"\n (ngModelChange)=\"setFieldSearchTerm($event)\"\n (keydown)=\"onSelectSearchKeydown($event)\"\n />\n </div>\n @for (field of filteredFieldOptions('time', comparisonPeriodFieldValue()); track field.field) {\n <mat-option [value]=\"field.field\" class=\"editor-rich-mat-option\">\n <div class=\"editor-rich-option\">\n <div class=\"editor-rich-option__body\">\n <div class=\"editor-rich-option__header\">\n <span class=\"editor-rich-option__label\">{{ field.label || field.field }}</span>\n @if (field.type) {\n <span class=\"editor-rich-option__badge\">{{ field.type }}</span>\n }\n </div>\n @if (field.label && field.label !== field.field) {\n <span class=\"editor-rich-option__code\">{{ field.field }}</span>\n }\n @if (field.description) {\n <span class=\"editor-rich-option__description\">{{ field.description }}</span>\n }\n </div>\n </div>\n </mat-option>\n }\n </mat-select>\n } @else {\n <mat-select [ngModel]=\"comparisonPeriodFieldValue()\" [disabled]=\"true\"></mat-select>\n }\n @if (!fieldOptions('time').length) {\n <mat-hint>{{ t('praxis.charts.editor.catalog.comparisonPeriodFieldsMissing', 'No governed time-series field is available for comparison periods.') }}</mat-hint>\n }\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.comparisonTimezone', 'Timezone') }}</mat-label>\n <input\n matInput\n [ngModel]=\"comparisonPeriodTimezoneValue()\"\n (ngModelChange)=\"setComparisonPeriodTimezone($event)\"\n [disabled]=\"isReadonly()\"\n />\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.comparisonPreset', 'Period preset') }}</mat-label>\n <mat-select\n [ngModel]=\"comparisonPeriodPresetValue()\"\n (ngModelChange)=\"setComparisonPeriodPreset($event)\"\n [disabled]=\"isReadonly()\"\n >\n @for (preset of comparisonPeriodPresets; track preset) {\n <mat-option [value]=\"preset\">\n {{ t('praxis.charts.editor.comparisonPreset.' + preset, preset) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.comparisonMode', 'Comparison mode') }}</mat-label>\n <mat-select\n [ngModel]=\"comparisonPeriodModeValue()\"\n (ngModelChange)=\"setComparisonPeriodMode($event)\"\n [disabled]=\"isReadonly()\"\n >\n @for (mode of comparisonPeriodModes; track mode) {\n <mat-option [value]=\"mode\">\n {{ t('praxis.charts.editor.comparisonMode.' + mode, mode) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n </mat-card-content>\n </mat-card>\n }\n\n @if (doc().source.kind === 'derived') {\n <mat-card class=\"editor-card\" data-testid=\"chart-editor-local-data\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.localData.title', 'Local dataset (inputs.data)') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-stack\">\n <p class=\"editor-caption\">\n {{ t('praxis.charts.editor.localData.caption', 'Edit the local records rendered when the data source is derived.') }}\n </p>\n\n @if (canConsolidateGaugeLocalData()) {\n <div class=\"editor-row-actions\">\n <button\n mat-stroked-button\n type=\"button\"\n data-testid=\"chart-local-data-consolidate-gauge\"\n (click)=\"consolidateGaugeLocalData()\"\n [disabled]=\"isReadonly()\"\n >\n {{ t('praxis.charts.editor.localData.consolidateGauge', 'Consolidate to 1 bucket for gauge') }}\n </button>\n </div>\n }\n\n @if (localDataRows().length) {\n @for (row of localDataRows(); track $index; let rowIndex = $index) {\n <div class=\"editor-row-card\" [attr.data-testid]=\"'chart-local-data-row-' + rowIndex\">\n <div class=\"editor-grid\">\n @for (dimField of localDataDimensionFields(); track dimField) {\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ dimField }}</mat-label>\n <input\n matInput\n [attr.data-testid]=\"'chart-local-data-dim-' + rowIndex + '-' + dimField\"\n [ngModel]=\"localDataCellStringValue(rowIndex, dimField)\"\n (ngModelChange)=\"setLocalDataCellValue(rowIndex, dimField, $event, 'dimension')\"\n [disabled]=\"isReadonly()\"\n />\n </mat-form-field>\n }\n @for (metricField of localDataMetricFields(); track metricField) {\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ metricField }}</mat-label>\n <input\n matInput\n type=\"number\"\n [attr.data-testid]=\"'chart-local-data-metric-' + rowIndex + '-' + metricField\"\n [ngModel]=\"localDataCellStringValue(rowIndex, metricField)\"\n (ngModelChange)=\"setLocalDataCellValue(rowIndex, metricField, $event, 'metric')\"\n [disabled]=\"isReadonly()\"\n />\n </mat-form-field>\n }\n </div>\n <div class=\"editor-row-actions\">\n <button\n mat-button\n type=\"button\"\n [attr.data-testid]=\"'chart-local-data-remove-row-' + rowIndex\"\n (click)=\"removeLocalDataRow(rowIndex)\"\n [disabled]=\"isReadonly()\"\n >\n {{ t('praxis.charts.editor.localData.removeRow', 'Remove record') }}\n </button>\n </div>\n </div>\n }\n } @else {\n <div class=\"editor-empty\" data-testid=\"chart-local-data-empty\">\n {{ t('praxis.charts.editor.localData.empty', 'No local records configured yet. Add a record to replace the synthetic preview sample.') }}\n </div>\n }\n\n <div>\n <button\n mat-stroked-button\n type=\"button\"\n data-testid=\"chart-local-data-add-row\"\n (click)=\"addLocalDataRow()\"\n [disabled]=\"isReadonly()\"\n >\n {{ t('praxis.charts.editor.localData.addRow', 'Add record') }}\n </button>\n </div>\n </mat-card-content>\n </mat-card>\n }\n </div>\n }\n\n @case ('motion') {\n <mat-card class=\"editor-card\">\n <mat-card-content class=\"editor-stack\">\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 <p class=\"editor-caption\" data-testid=\"chart-editor-motion-hint\">\n {{ t('praxis.charts.editor.motion.hint', 'Controls initial entrance timing and smooth transitions when filters or query context change, with automatic volume thresholds for high-density datasets.') }}\n </p>\n\n <div>\n <button\n mat-stroked-button\n type=\"button\"\n data-testid=\"chart-editor-simulate-motion\"\n (click)=\"simulateMotionTransition()\"\n [disabled]=\"normalizedDocument().motion?.enabled === false || !effectivePreview()\"\n >\n {{ t('praxis.charts.editor.motion.simulate', 'Simulate transition') }}\n </button>\n </div>\n </mat-card-content>\n </mat-card>\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-toggle-cluster\">\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 <div class=\"editor-grid\">\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 </div>\n\n @if (canApplyDomainSemanticPalette()) {\n <div class=\"editor-row-actions\">\n <button\n mat-stroked-button\n type=\"button\"\n data-testid=\"chart-editor-apply-domain-palette\"\n (click)=\"applyDomainSemanticPalette()\"\n [disabled]=\"isReadonly()\"\n [title]=\"t('praxis.charts.editor.appearance.applyDomainPaletteHint', 'Generates a custom palette with semantic colors (success, warning, danger, info) from the dimension categories.')\"\n >\n <mat-icon aria-hidden=\"true\">auto_fix_high</mat-icon>\n {{ t('praxis.charts.editor.appearance.applyDomainPalette', 'Apply domain semantic colors') }}\n </button>\n </div>\n }\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\n <div class=\"editor-palette-swatches\" data-testid=\"chart-editor-palette-swatches\">\n @for (swatch of tokenPaletteSwatches(); track $index) {\n <span class=\"editor-palette-swatch-chip\" [title]=\"swatch\">\n <span class=\"editor-palette-swatch-dot\" [style.background]=\"swatch\"></span>\n <code class=\"editor-palette-swatch-code\">{{ swatch }}</code>\n </span>\n }\n </div>\n } @else {\n <div class=\"editor-palette-swatches\" data-testid=\"chart-editor-palette-swatches\">\n @for (swatch of customPaletteColors(); track $index) {\n <span class=\"editor-palette-swatch-chip\" [title]=\"swatch\">\n <span class=\"editor-palette-swatch-dot\" [style.background]=\"swatch\"></span>\n <input\n type=\"color\"\n class=\"editor-palette-color-input\"\n [attr.data-testid]=\"'chart-editor-palette-color-' + $index\"\n [value]=\"swatchHexValue(swatch, $index)\"\n (input)=\"setCustomPaletteColorAt($index, $any($event.target).value)\"\n [disabled]=\"isReadonly()\"\n />\n <code class=\"editor-palette-swatch-code\">{{ swatch }}</code>\n <button\n type=\"button\"\n class=\"editor-palette-swatch-remove\"\n [attr.data-testid]=\"'chart-editor-palette-remove-' + $index\"\n (click)=\"removeCustomPaletteColor($index)\"\n [disabled]=\"isReadonly()\"\n [attr.aria-label]=\"t('praxis.charts.editor.appearance.removePaletteColor', 'Remove color')\"\n >\n \u00D7\n </button>\n </span>\n }\n <button\n mat-stroked-button\n type=\"button\"\n class=\"editor-palette-add-btn\"\n data-testid=\"chart-editor-add-palette-color\"\n (click)=\"addCustomPaletteColor()\"\n [disabled]=\"isReadonly()\"\n >\n + {{ t('praxis.charts.editor.appearance.addPaletteColor', 'Add color') }}\n </button>\n </div>\n\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=\"2\"\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-state-group\">\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=\"1\" [ngModel]=\"stateDescription('empty')\" (ngModelChange)=\"setStateDescription('empty', $event)\" [disabled]=\"isReadonly()\"></textarea>\n </mat-form-field>\n </div>\n </div>\n\n <div class=\"editor-state-group\">\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=\"1\" [ngModel]=\"stateDescription('loading')\" (ngModelChange)=\"setStateDescription('loading', $event)\" [disabled]=\"isReadonly()\"></textarea>\n </mat-form-field>\n </div>\n </div>\n\n <div class=\"editor-state-group\">\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=\"1\" [ngModel]=\"stateDescription('error')\" (ngModelChange)=\"setStateDescription('error', $event)\" [disabled]=\"isReadonly()\"></textarea>\n </mat-form-field>\n </div>\n </div>\n </mat-card-content>\n </mat-card>\n </div>\n }\n\n @case ('analytics') {\n <div class=\"editor-stack\">\n @if (showGaugePanel()) {\n <mat-card class=\"editor-card\" data-testid=\"chart-editor-gauge-scale\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.specialization.gaugeTitle', 'Gauge scale') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-stack\">\n <p class=\"editor-caption\">\n {{ t('praxis.charts.editor.specialization.gaugeHint', 'Gauge charts render exactly one bucket and one metric. Remote stats use group-by with limit 1; values outside the scale fail instead of being clamped.') }}\n </p>\n <div class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.gaugeMin', 'Scale minimum') }}</mat-label>\n <input\n matInput\n type=\"number\"\n data-testid=\"chart-editor-gauge-min\"\n [ngModel]=\"gaugeMinValue()\"\n (ngModelChange)=\"setGaugeMin($event)\"\n [disabled]=\"isReadonly()\"\n />\n </mat-form-field>\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.gaugeMax', 'Scale maximum') }}</mat-label>\n <input\n matInput\n type=\"number\"\n data-testid=\"chart-editor-gauge-max\"\n [ngModel]=\"gaugeMaxValue()\"\n (ngModelChange)=\"setGaugeMax($event)\"\n [disabled]=\"isReadonly()\"\n />\n </mat-form-field>\n </div>\n </mat-card-content>\n </mat-card>\n }\n\n @if (showComboPanel()) {\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.specialization.comboTitle', 'Combo guidance') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content>\n <p class=\"editor-caption\">\n {{ t('praxis.charts.editor.specialization.comboHint', 'Combo charts require at least two metrics and allow per-metric axis and series kind mapping.') }}\n </p>\n </mat-card-content>\n </mat-card>\n }\n\n @if (showPieDonutPanel()) {\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.specialization.pieDonutTitle', 'Composition guidance') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content>\n <p class=\"editor-caption\">\n {{ t('praxis.charts.editor.specialization.pieDonutHint', 'Pie, donut, funnel, pyramid and treemap charts keep one metric and use the first dimension as the category segment.') }}\n </p>\n </mat-card-content>\n </mat-card>\n }\n\n @if (showScatterPanel()) {\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.specialization.scatterTitle', 'Scatter guidance') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content>\n <p class=\"editor-caption\">\n {{ t('praxis.charts.editor.specialization.scatterHint', 'With one metric, scatter uses the first dimension as X and the metric as Y. With two metrics, the dimension identifies the grouped bucket and the metrics map to X and Y.') }}\n </p>\n </mat-card-content>\n </mat-card>\n }\n\n @if (showHeatmapPanel()) {\n <mat-card class=\"editor-card\" data-testid=\"chart-editor-heatmap-panel\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.specialization.heatmapTitle', 'Heatmap matrix guidance') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content>\n <p class=\"editor-caption\">\n {{ t('praxis.charts.editor.specialization.heatmapHint', 'Heatmap charts render a 2D matrix requiring two dimensions (X and Y axes) and one metric, using group-by for remote stats.') }}\n </p>\n </mat-card-content>\n </mat-card>\n }\n\n @if (canApplySemanticAnalyticsMapping()) {\n <div class=\"editor-row-actions\">\n <button\n mat-stroked-button\n type=\"button\"\n data-testid=\"chart-editor-smart-map-analytics\"\n (click)=\"applySemanticAnalyticsMapping()\"\n [disabled]=\"isReadonly()\"\n [title]=\"t('praxis.charts.editor.analytics.smartMapHint', 'Automatically selects the best dimension, metric, and aggregation from the governed field catalog.')\"\n >\n <mat-icon aria-hidden=\"true\">auto_fix_high</mat-icon>\n {{ t('praxis.charts.editor.analytics.smartMap', 'Suggest dimension & metric from schema') }}\n </button>\n </div>\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\n [ngModel]=\"dimension.field || ''\"\n (ngModelChange)=\"setDimensionField($index, $event)\"\n (openedChange)=\"setFieldSearchTerm('')\"\n [disabled]=\"isReadonly()\"\n >\n <div class=\"editor-select-search\" (click)=\"$event.stopPropagation()\">\n <mat-icon aria-hidden=\"true\">search</mat-icon>\n <input\n type=\"search\"\n [placeholder]=\"t('praxis.charts.editor.field.fieldSearchPlaceholder', 'Search field...')\"\n [ngModel]=\"fieldSearchTerm()\"\n (ngModelChange)=\"setFieldSearchTerm($event)\"\n (keydown)=\"onSelectSearchKeydown($event)\"\n />\n </div>\n @for (field of filteredFieldOptions('dimension', dimension.field); track field.field) {\n <mat-option [value]=\"field.field\" class=\"editor-rich-mat-option\">\n <div class=\"editor-rich-option\">\n <div class=\"editor-rich-option__body\">\n <div class=\"editor-rich-option__header\">\n <span class=\"editor-rich-option__label\">{{ field.label || field.field }}</span>\n @if (field.type) {\n <span class=\"editor-rich-option__badge\">{{ field.type }}</span>\n }\n </div>\n @if (field.label && field.label !== field.field) {\n <span class=\"editor-rich-option__code\">{{ field.field }}</span>\n }\n @if (field.description) {\n <span class=\"editor-rich-option__description\">{{ field.description }}</span>\n }\n </div>\n </div>\n </mat-option>\n }\n </mat-select>\n } @else {\n <mat-select [ngModel]=\"dimension.field || ''\" (ngModelChange)=\"setDimensionField($index, $event)\" [disabled]=\"true\"></mat-select>\n }\n @if (!fieldOptions('dimension').length) {\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\n <mat-form-field class=\"editor-field\" appearance=\"outline\" [attr.data-testid]=\"'chart-editor-dimension-extraction-' + $index\">\n <mat-label>{{ t('praxis.charts.editor.field.dimensionExtraction', 'Temporal extraction') }}</mat-label>\n <mat-select [ngModel]=\"dimension.extraction || ''\" (ngModelChange)=\"setDimensionExtraction($index, $event)\" [disabled]=\"isReadonly()\">\n <mat-option [value]=\"''\">\n {{ t('praxis.charts.editor.dimensionExtraction.none', 'None') }}\n </mat-option>\n @for (extraction of dimensionExtractions; track extraction) {\n <mat-option [value]=\"extraction\">\n {{ t('praxis.charts.editor.dimensionExtraction.' + extraction, extraction) }}\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() || !canRemoveDimension()\">\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() || !canAddDimension()\">\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\n [ngModel]=\"metric.field || ''\"\n (ngModelChange)=\"setMetricField($index, $event)\"\n (openedChange)=\"setFieldSearchTerm('')\"\n [disabled]=\"isReadonly()\"\n >\n <div class=\"editor-select-search\" (click)=\"$event.stopPropagation()\">\n <mat-icon aria-hidden=\"true\">search</mat-icon>\n <input\n type=\"search\"\n [placeholder]=\"t('praxis.charts.editor.field.fieldSearchPlaceholder', 'Search field...')\"\n [ngModel]=\"fieldSearchTerm()\"\n (ngModelChange)=\"setFieldSearchTerm($event)\"\n (keydown)=\"onSelectSearchKeydown($event)\"\n />\n </div>\n @for (field of filteredFieldOptions('metric', metric.field); track field.field) {\n <mat-option [value]=\"field.field\" class=\"editor-rich-mat-option\">\n <div class=\"editor-rich-option\">\n <div class=\"editor-rich-option__body\">\n <div class=\"editor-rich-option__header\">\n <span class=\"editor-rich-option__label\">{{ field.label || field.field }}</span>\n @if (field.type) {\n <span class=\"editor-rich-option__badge\">{{ field.type }}</span>\n }\n </div>\n @if (field.label && field.label !== field.field) {\n <span class=\"editor-rich-option__code\">{{ field.field }}</span>\n }\n @if (field.description) {\n <span class=\"editor-rich-option__description\">{{ field.description }}</span>\n }\n </div>\n </div>\n </mat-option>\n }\n </mat-select>\n } @else {\n <mat-select [ngModel]=\"metric.field || ''\" (ngModelChange)=\"setMetricField($index, $event)\" [disabled]=\"true\"></mat-select>\n }\n @if (!fieldOptions('metric').length) {\n <mat-hint>{{ t('praxis.charts.editor.catalog.metricFieldsMissing', 'No governed metrics are available for the current resource and operation.') }}</mat-hint>\n }\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\" [attr.data-testid]=\"'chart-editor-metric-label-' + $index\">\n <mat-label>{{ t('praxis.charts.editor.field.metricLabel', 'Metric label') }}</mat-label>\n <input matInput [ngModel]=\"metric.label || ''\" (ngModelChange)=\"setMetricLabel($index, $event)\" [disabled]=\"isReadonly()\" />\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\" [attr.data-testid]=\"'chart-editor-metric-aggregation-' + $index\">\n <mat-label>{{ t('praxis.charts.editor.field.metricAggregation', 'Aggregation') }}</mat-label>\n <mat-select [ngModel]=\"metric.aggregation || 'sum'\" (ngModelChange)=\"setMetricAggregation($index, $event)\" [disabled]=\"isReadonly()\">\n @for (aggregation of metricAggregationOptions(metric.field); track aggregation) {\n <mat-option [value]=\"aggregation\">\n {{ t('praxis.charts.editor.metricAggregation.' + aggregation, aggregation) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n @if (showMetricAxisControls()) {\n <mat-form-field class=\"editor-field\" appearance=\"outline\" [attr.data-testid]=\"'chart-editor-metric-axis-' + $index\">\n <mat-label>{{ t('praxis.charts.editor.field.metricAxis', 'Axis') }}</mat-label>\n <mat-select [ngModel]=\"metric.axis || 'primary'\" (ngModelChange)=\"setMetricAxis($index, $event)\" [disabled]=\"isReadonly()\">\n @for (axis of metricAxes; track axis) {\n <mat-option [value]=\"axis\">\n {{ t('praxis.charts.editor.metricAxis.' + axis, axis) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n }\n\n @if (showMetricSeriesKindControls()) {\n <mat-form-field class=\"editor-field\" appearance=\"outline\" [attr.data-testid]=\"'chart-editor-metric-series-kind-' + $index\">\n <mat-label>{{ t('praxis.charts.editor.field.metricSeriesKind', 'Series kind') }}</mat-label>\n <mat-select [ngModel]=\"metric.seriesKind || 'bar'\" (ngModelChange)=\"setMetricSeriesKind($index, $event)\" [disabled]=\"isReadonly()\">\n @for (seriesKind of metricSeriesKinds; track seriesKind) {\n <mat-option [value]=\"seriesKind\">\n {{ t('praxis.charts.editor.metricSeriesKind.' + seriesKind, seriesKind) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n }\n </div>\n\n <div class=\"editor-row-actions\">\n <button mat-button type=\"button\" [attr.data-testid]=\"'chart-editor-remove-metric-' + $index\" (click)=\"removeMetric($index)\" [disabled]=\"isReadonly() || metrics().length <= 1\">\n {{ t('praxis.charts.editor.analytics.removeMetric', 'Remove metric') }}\n </button>\n </div>\n </div>\n }\n\n <div>\n <button mat-stroked-button type=\"button\" data-testid=\"chart-editor-add-metric\" (click)=\"addMetric()\" [disabled]=\"isReadonly() || !canAddMetric()\">\n {{ t('praxis.charts.editor.analytics.addMetric', 'Add metric') }}\n </button>\n </div>\n </mat-card-content>\n </mat-card>\n </div>\n }\n\n @case ('events') {\n <div class=\"editor-stack\">\n <mat-card class=\"editor-card\" data-testid=\"chart-editor-event-pointClick\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.events.pointClickTitle', 'Point click') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventAction', 'Action') }}</mat-label>\n <mat-select data-testid=\"chart-editor-event-pointClick-action\" [ngModel]=\"eventAction('pointClick')\" (ngModelChange)=\"setEventAction('pointClick', $event)\" [disabled]=\"isReadonly()\">\n <mat-option value=\"\">{{ t('praxis.charts.editor.events.none', 'None') }}</mat-option>\n @for (action of eventActionOptions; track action) {\n <mat-option [value]=\"action\">\n {{ t('praxis.charts.editor.eventAction.' + action, action) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventTarget', 'Target') }}</mat-label>\n <mat-select data-testid=\"chart-editor-event-pointClick-target\" [ngModel]=\"eventTarget('pointClick')\" (ngModelChange)=\"setEventTarget('pointClick', $event)\" [disabled]=\"isReadonly() || !eventAction('pointClick') || targetCatalogUnavailable(eventAction('pointClick'), 'pointClick')\">\n @for (target of targetOptions(eventAction('pointClick'), 'pointClick'); track target.id) {\n <mat-option [value]=\"target.id\">{{ target.label }}</mat-option>\n }\n </mat-select>\n @if (targetCatalogUnavailable(eventAction('pointClick'), 'pointClick')) {\n <mat-hint>{{ t('praxis.charts.editor.catalog.targetMissing', 'Target catalog is required for this governed event action.') }}</mat-hint>\n }\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventMapping', 'Mapping') }}</mat-label>\n <textarea\n matInput\n data-testid=\"chart-editor-event-pointClick-mapping\"\n rows=\"4\"\n [ngModel]=\"eventMappingText('pointClick')\"\n (ngModelChange)=\"setEventMapping('pointClick', $event)\"\n [disabled]=\"isReadonly() || !eventAction('pointClick')\"\n ></textarea>\n </mat-form-field>\n </mat-card-content>\n </mat-card>\n\n <mat-card class=\"editor-card\" data-testid=\"chart-editor-event-selectionChange\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.events.selectionChangeTitle', 'Selection change') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventAction', 'Action') }}</mat-label>\n <mat-select data-testid=\"chart-editor-event-selectionChange-action\" [ngModel]=\"eventAction('selectionChange')\" (ngModelChange)=\"setEventAction('selectionChange', $event)\" [disabled]=\"isReadonly()\">\n <mat-option value=\"\">{{ t('praxis.charts.editor.events.none', 'None') }}</mat-option>\n @for (action of eventActionOptions; track action) {\n <mat-option [value]=\"action\">\n {{ t('praxis.charts.editor.eventAction.' + action, action) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventTarget', 'Target') }}</mat-label>\n <mat-select data-testid=\"chart-editor-event-selectionChange-target\" [ngModel]=\"eventTarget('selectionChange')\" (ngModelChange)=\"setEventTarget('selectionChange', $event)\" [disabled]=\"isReadonly() || !eventAction('selectionChange') || targetCatalogUnavailable(eventAction('selectionChange'), 'selectionChange')\">\n @for (target of targetOptions(eventAction('selectionChange'), 'selectionChange'); track target.id) {\n <mat-option [value]=\"target.id\">{{ target.label }}</mat-option>\n }\n </mat-select>\n @if (targetCatalogUnavailable(eventAction('selectionChange'), 'selectionChange')) {\n <mat-hint>{{ t('praxis.charts.editor.catalog.targetMissing', 'Target catalog is required for this governed event action.') }}</mat-hint>\n }\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventMapping', 'Mapping') }}</mat-label>\n <textarea\n matInput\n data-testid=\"chart-editor-event-selectionChange-mapping\"\n rows=\"4\"\n [ngModel]=\"eventMappingText('selectionChange')\"\n (ngModelChange)=\"setEventMapping('selectionChange', $event)\"\n [disabled]=\"isReadonly() || !eventAction('selectionChange')\"\n ></textarea>\n </mat-form-field>\n </mat-card-content>\n </mat-card>\n\n <mat-card class=\"editor-card\" data-testid=\"chart-editor-event-drillDown\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.events.drillDownTitle', 'Drill down') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventAction', 'Action') }}</mat-label>\n <mat-select data-testid=\"chart-editor-event-drillDown-action\" [ngModel]=\"eventAction('drillDown')\" (ngModelChange)=\"setEventAction('drillDown', $event)\" [disabled]=\"isReadonly()\">\n <mat-option value=\"\">{{ t('praxis.charts.editor.events.none', 'None') }}</mat-option>\n @for (action of eventActionOptions; track action) {\n <mat-option [value]=\"action\">\n {{ t('praxis.charts.editor.eventAction.' + action, action) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventTarget', 'Target') }}</mat-label>\n <mat-select data-testid=\"chart-editor-event-drillDown-target\" [ngModel]=\"eventTarget('drillDown')\" (ngModelChange)=\"setEventTarget('drillDown', $event)\" [disabled]=\"isReadonly() || !eventAction('drillDown') || targetCatalogUnavailable(eventAction('drillDown'), 'drillDown')\">\n @for (target of targetOptions(eventAction('drillDown'), 'drillDown'); track target.id) {\n <mat-option [value]=\"target.id\">{{ target.label }}</mat-option>\n }\n </mat-select>\n @if (targetCatalogUnavailable(eventAction('drillDown'), 'drillDown')) {\n <mat-hint>{{ t('praxis.charts.editor.catalog.targetMissing', 'Target catalog is required for this governed event action.') }}</mat-hint>\n }\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventMapping', 'Mapping') }}</mat-label>\n <textarea\n matInput\n data-testid=\"chart-editor-event-drillDown-mapping\"\n rows=\"4\"\n [ngModel]=\"eventMappingText('drillDown')\"\n (ngModelChange)=\"setEventMapping('drillDown', $event)\"\n [disabled]=\"isReadonly() || !eventAction('drillDown')\"\n ></textarea>\n </mat-form-field>\n </mat-card-content>\n </mat-card>\n\n <mat-card class=\"editor-card\" data-testid=\"chart-editor-event-crossFilter\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.events.crossFilterTitle', 'Cross filter') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventAction', 'Action') }}</mat-label>\n <mat-select data-testid=\"chart-editor-event-crossFilter-action\" [ngModel]=\"eventAction('crossFilter')\" (ngModelChange)=\"setEventAction('crossFilter', $event)\" [disabled]=\"isReadonly()\">\n <mat-option value=\"\">{{ t('praxis.charts.editor.events.none', 'None') }}</mat-option>\n @for (action of eventActionOptions; track action) {\n <mat-option [value]=\"action\">\n {{ t('praxis.charts.editor.eventAction.' + action, action) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventTarget', 'Target') }}</mat-label>\n <mat-select data-testid=\"chart-editor-event-crossFilter-target\" [ngModel]=\"eventTarget('crossFilter')\" (ngModelChange)=\"setEventTarget('crossFilter', $event)\" [disabled]=\"isReadonly() || !eventAction('crossFilter') || targetCatalogUnavailable(eventAction('crossFilter'), 'crossFilter')\">\n @for (target of targetOptions(eventAction('crossFilter'), 'crossFilter'); track target.id) {\n <mat-option [value]=\"target.id\">{{ target.label }}</mat-option>\n }\n </mat-select>\n @if (targetCatalogUnavailable(eventAction('crossFilter'), 'crossFilter')) {\n <mat-hint>{{ t('praxis.charts.editor.catalog.targetMissing', 'Target catalog is required for this governed event action.') }}</mat-hint>\n }\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventMapping', 'Mapping') }}</mat-label>\n <textarea\n matInput\n data-testid=\"chart-editor-event-crossFilter-mapping\"\n rows=\"4\"\n [ngModel]=\"eventMappingText('crossFilter')\"\n (ngModelChange)=\"setEventMapping('crossFilter', $event)\"\n [disabled]=\"isReadonly() || !eventAction('crossFilter')\"\n ></textarea>\n </mat-form-field>\n </mat-card-content>\n </mat-card>\n </div>\n }\n\n @case ('preview') {\n <mat-card class=\"editor-card\">\n <mat-card-content>\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 </mat-card-content>\n </mat-card>\n }\n }\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\" [class.editor-issue--warning]=\"issue.severity === 'warning'\">\n <div class=\"editor-issue__header\">\n <strong>{{ issue.field }}</strong>\n @if (issueTargetSection(issue); as targetSection) {\n <button\n mat-button\n type=\"button\"\n class=\"editor-issue__jump\"\n [attr.data-testid]=\"'chart-editor-issue-jump-' + issue.code\"\n [attr.aria-label]=\"t('praxis.charts.editor.issues.jumpToSection', 'Go to {{ section }}', { section: sectionLabel(targetSection) })\"\n (click)=\"setSection(targetSection)\"\n >\n {{ sectionLabel(targetSection) }} \u2192\n </button>\n }\n </div>\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()) {\n <div\n class=\"editor-empty editor-empty--actionable editor-preview-degraded\"\n data-testid=\"chart-editor-preview-degraded-warning\"\n >\n <span>{{\n effectivePreview()\n ? t('praxis.charts.editor.preview.degradedNotice', 'Aesthetic simulation active with synthetic sample data while contract validation issues remain open.')\n : t('praxis.charts.editor.preview.invalid', 'Preview is unavailable while the contract has blocking errors.')\n }}</span>\n @if (firstInvalidSection(); as invalidSection) {\n <button\n mat-stroked-button\n type=\"button\"\n data-testid=\"chart-editor-preview-jump-invalid\"\n [attr.aria-label]=\"t('praxis.charts.editor.issues.jumpToSection', 'Go to {{ section }}', { section: sectionLabel(invalidSection) })\"\n (click)=\"setSection(invalidSection)\"\n >\n {{ sectionLabel(invalidSection) }} \u2192\n </button>\n }\n </div>\n }\n @if (effectivePreview(); 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 <p class=\"editor-caption\" data-testid=\"chart-editor-preview-origin\">\n {{\n doc().source.kind === 'praxis.stats'\n ? t('praxis.charts.editor.preview.originStatsSynthetic', 'Previewing synthetic sample for governed backend stats (praxis.stats).')\n : chartPreview.dataOrigin === 'local'\n ? t('praxis.charts.editor.preview.originLocal', 'Previewing widget local dataset (inputs.data).')\n : t('praxis.charts.editor.preview.originSynthetic', 'Previewing synthetic sample data (no local dataset records).')\n }}\n </p>\n <praxis-chart [config]=\"chartPreview.config\" [data]=\"chartPreview.data\"></praxis-chart>\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:16px;min-width:0}.editor-nav{display:flex;gap:6px;flex-wrap:wrap;padding:4px;border-radius:14px;background:var(--md-sys-color-surface-container-low, rgba(18, 99, 180, .04));border:1px solid color-mix(in srgb,var(--md-sys-color-outline-variant, var(--md-sys-color-outline, #c5c7ce)) 64%,transparent)}.editor-nav button{border-radius:10px;border-color:transparent;display:inline-flex;align-items:center;gap:6px}.editor-nav button.active{background:color-mix(in srgb,var(--pfx-settings-panel-primary, var(--md-sys-color-primary, #1263b4)) 22%,transparent);color:var(--pfx-settings-panel-primary, var(--md-sys-color-primary, #1263b4));border-color:color-mix(in srgb,var(--pfx-settings-panel-primary, var(--md-sys-color-primary, #1263b4)) 48%,transparent);font-weight:600}.editor-nav__badge{display:inline-flex;align-items:center;justify-content:center;min-width:18px;height:18px;padding:0 5px;margin-left:6px;border-radius:999px;font-size:.7rem;font-weight:700;line-height:1;background:color-mix(in srgb,var(--md-sys-color-tertiary, #f59e0b) 24%,transparent);color:var(--pfx-settings-panel-fg, var(--md-sys-color-on-surface, inherit))}.editor-nav__badge--error{background:color-mix(in srgb,var(--pfx-settings-panel-error, var(--md-sys-color-error, #b3261e)) 24%,transparent);color:var(--pfx-settings-panel-error, var(--md-sys-color-error, #b3261e))}:host ::ng-deep .mat-mdc-form-field-hint-wrapper,:host ::ng-deep .mat-mdc-form-field-error-wrapper{position:relative!important;top:auto!important;left:auto!important;right:auto!important;padding-top:4px;line-height:1.35}:host ::ng-deep .mat-mdc-form-field-bottom-align:before{display:none}.editor-layout{display:grid;gap:16px;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:16px;border:1px solid color-mix(in srgb,var(--md-sys-color-outline-variant, var(--md-sys-color-outline, #c5c7ce)) 72%,transparent);background:var(--md-sys-color-surface-container-low, rgba(18, 99, 180, .03))}.editor-grid{display:grid;gap:14px;grid-template-columns:repeat(auto-fit,minmax(210px,1fr));align-items:start}.editor-toggle-cluster{display:flex;flex-wrap:wrap;align-items:center;gap:14px 24px}.editor-stack{display:grid;gap:14px}.editor-state-group{padding-top:14px;border-top:1px solid color-mix(in srgb,var(--md-sys-color-outline-variant, var(--md-sys-color-outline, #c5c7ce)) 48%,transparent)}.editor-state-group:first-child{padding-top:0;border-top:0}.editor-palette-swatches{display:flex;flex-wrap:wrap;align-items:center;gap:8px}.editor-palette-swatch-chip{display:inline-flex;align-items:center;gap:6px;padding:4px 8px;border-radius:999px;border:1px solid color-mix(in srgb,var(--md-sys-color-outline-variant, var(--md-sys-color-outline, #c5c7ce)) 68%,transparent);background:color-mix(in srgb,var(--pfx-settings-panel-surface, var(--md-sys-color-surface-container, transparent)) 88%,transparent);font-size:.76rem}.editor-palette-swatch-dot{width:16px;height:16px;border-radius:50%;border:1px solid rgba(128,128,128,.35);flex-shrink:0}.editor-palette-color-input{width:22px;height:22px;padding:0;border:0;border-radius:50%;background:transparent;cursor:pointer}.editor-palette-swatch-code{font-family:var(--pdx-font-mono, ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace);font-size:.74rem;color:var(--md-sys-color-on-surface, inherit)}.editor-palette-swatch-remove{appearance:none;border:0;background:transparent;color:var(--md-sys-color-on-surface-variant, #5a5d67);font-size:.95rem;line-height:1;padding:0 2px;cursor:pointer}.editor-row-card{padding:14px;border-radius:14px;border:1px solid color-mix(in srgb,var(--md-sys-color-outline-variant, var(--md-sys-color-outline, #c5c7ce)) 54%,transparent);background:color-mix(in srgb,var(--pfx-settings-panel-surface, var(--md-sys-color-surface-container, transparent)) 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:12px;background:color-mix(in srgb,var(--md-sys-color-error, #b3261e) 10%,transparent);border:1px solid color-mix(in srgb,var(--md-sys-color-error, #b3261e) 24%,transparent)}.editor-issue--warning{background:color-mix(in srgb,var(--md-sys-color-tertiary, #f59e0b) 10%,transparent);border-color:color-mix(in srgb,var(--md-sys-color-tertiary, #f59e0b) 26%,transparent)}.editor-issue__header{display:flex;align-items:center;justify-content:space-between;gap:8px;flex-wrap:wrap;margin-bottom:4px}.editor-issue strong{display:block;font-size:.84rem}.editor-issue__jump{min-height:26px;padding:0 8px;font-size:.76rem}.editor-caption{margin:0 0 12px;color:var(--md-sys-color-on-surface-variant, #5a5d67);font-size:.88rem;line-height:1.45}.editor-empty{padding:16px;border-radius:12px;background:color-mix(in srgb,var(--md-sys-color-surface-variant, #eceff4) 52%,transparent);border:1px dashed color-mix(in srgb,var(--md-sys-color-outline-variant, var(--md-sys-color-outline, #c5c7ce)) 64%,transparent);color:var(--md-sys-color-on-surface-variant, #5a5d67);font-size:.88rem}.editor-empty--actionable{display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap}.editor-select-trigger{display:inline-flex;align-items:center;gap:8px;min-width:0;max-width:100%}.editor-select-trigger__icon{width:18px;height:18px;font-size:18px;flex-shrink:0;color:var(--md-sys-color-primary, #1263b4)}.editor-select-trigger__label{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-weight:500}.editor-select-trigger__code{flex-shrink:0;max-width:45%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-family:var(--pdx-font-mono, ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace);font-size:.72rem;padding:1px 6px;border-radius:6px;background:color-mix(in srgb,currentColor 8%,transparent);color:color-mix(in srgb,currentColor 72%,transparent)}::ng-deep .editor-select-search{position:sticky;top:0;z-index:2;display:flex;align-items:center;gap:8px;padding:8px 12px;background:var(--md-sys-color-surface-container-highest, #f4f6fa);border-bottom:1px solid color-mix(in srgb,var(--md-sys-color-outline, #c5c7ce) 45%,transparent)}::ng-deep .editor-select-search mat-icon{width:18px;height:18px;font-size:18px;color:var(--md-sys-color-on-surface-variant, #5a5d67);flex-shrink:0}::ng-deep .editor-select-search input{width:100%;border:0;outline:none;background:transparent;color:inherit;font:inherit;font-size:.85rem}::ng-deep .mat-mdc-option.editor-rich-mat-option{min-height:54px;height:auto;padding-block:8px;align-items:flex-start}::ng-deep .editor-rich-option{display:flex;align-items:flex-start;gap:10px;min-width:0;width:100%;line-height:1.25}::ng-deep .editor-rich-option__icon{width:28px;height:28px;border-radius:8px;background:color-mix(in srgb,var(--md-sys-color-primary, #1263b4) 12%,transparent);color:var(--md-sys-color-primary, #1263b4);display:inline-flex;align-items:center;justify-content:center;flex-shrink:0;margin-top:1px}::ng-deep .editor-rich-option__icon mat-icon{width:16px;height:16px;font-size:16px}::ng-deep .editor-rich-option__body{display:flex;flex-direction:column;gap:2px;min-width:0;flex:1 1 auto}::ng-deep .editor-rich-option__header{display:flex;align-items:center;gap:6px;flex-wrap:wrap;min-width:0}::ng-deep .editor-rich-option__label{font-weight:600;font-size:.88rem;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}::ng-deep .editor-rich-option__badge,.editor-resource-summary__badge{display:inline-flex;align-items:center;padding:1px 6px;border-radius:999px;font-size:.68rem;font-weight:600;background:color-mix(in srgb,currentColor 10%,transparent);color:color-mix(in srgb,currentColor 78%,transparent);white-space:nowrap}::ng-deep .editor-rich-option__code,.editor-resource-summary__code{font-family:var(--pdx-font-mono, ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace);font-size:.72rem;color:var(--md-sys-color-primary, #1263b4);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}::ng-deep .editor-rich-option__description,.editor-resource-summary__description{margin:0;font-size:.75rem;color:var(--md-sys-color-on-surface-variant, #5a5d67);display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;white-space:normal}.editor-resource-summary{display:flex;align-items:flex-start;gap:10px;padding:10px 12px;border-radius:12px;background:color-mix(in srgb,var(--md-sys-color-primary, #1263b4) 6%,transparent);border:1px solid color-mix(in srgb,var(--md-sys-color-primary, #1263b4) 18%,transparent)}.editor-resource-summary__icon{width:28px;height:28px;border-radius:8px;background:color-mix(in srgb,var(--md-sys-color-primary, #1263b4) 12%,transparent);color:var(--md-sys-color-primary, #1263b4);display:inline-flex;align-items:center;justify-content:center;flex-shrink:0;margin-top:1px}.editor-resource-summary__icon mat-icon{width:16px;height:16px;font-size:16px}.editor-resource-summary__body{display:grid;gap:2px;min-width:0;flex:1 1 auto}.editor-resource-summary__header{display:flex;align-items:center;gap:6px;flex-wrap:wrap}.editor-side praxis-chart{display:block;width:100%;min-width:0;max-width:100%}.editor-preview-degraded{margin-bottom:12px;background:color-mix(in srgb,var(--md-sys-color-tertiary, #f59e0b) 10%,transparent);border-style:solid;border-color:color-mix(in srgb,var(--md-sys-color-tertiary, #f59e0b) 32%,transparent)}@container praxis-chart-editor (min-width: 841px){.editor-nav__preview-btn:not(.active){display:none}}@container praxis-chart-editor (max-width: 840px){.editor-layout{grid-template-columns:minmax(0,1fr)}.editor-shell--preview-active .editor-form{display:none}}@media(max-width:960px){.editor-layout{grid-template-columns:minmax(0,1fr)}}\n"] }]
|
|
14928
16624
|
}], ctorParameters: () => [], propDecorators: { documentInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "document", required: false }] }], localDataInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "localData", 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"] }], localDataChange: [{ type: i0.Output, args: ["localDataChange"] }] } });
|
|
14929
16625
|
|
|
14930
16626
|
class PraxisChartWidgetConfigEditor {
|
|
@@ -15668,14 +17364,15 @@ const PRAXIS_CHART_AUTHORING_COVERAGE = [
|
|
|
15668
17364
|
];
|
|
15669
17365
|
|
|
15670
17366
|
const chartStatsOperations = ['group-by', 'timeseries', 'distribution', 'comparison'];
|
|
15671
|
-
const chartKinds = ['bar', 'combo', 'horizontal-bar', 'line', 'pie', 'donut', 'area', 'stacked-bar', 'stacked-area', 'scatter', 'funnel', 'pyramid', 'treemap', 'gauge'];
|
|
17367
|
+
const chartKinds = ['bar', 'combo', 'horizontal-bar', 'line', 'pie', 'donut', 'area', 'stacked-bar', 'stacked-area', 'scatter', 'funnel', 'pyramid', 'treemap', 'gauge', 'heatmap'];
|
|
17368
|
+
const chartComparisonPeriodPresets = ['TODAY', 'YESTERDAY', 'LAST_7_DAYS', 'LAST_30_DAYS', 'THIS_MONTH', 'LAST_MONTH', 'THIS_QUARTER', 'THIS_YEAR'];
|
|
15672
17369
|
const chartComparisonPeriodSchema = {
|
|
15673
17370
|
type: 'object',
|
|
15674
17371
|
required: ['field', 'timezone', 'preset', 'mode'],
|
|
15675
17372
|
properties: {
|
|
15676
17373
|
field: { type: 'string' },
|
|
15677
17374
|
timezone: { type: 'string' },
|
|
15678
|
-
preset: { enum:
|
|
17375
|
+
preset: { enum: chartComparisonPeriodPresets },
|
|
15679
17376
|
mode: { enum: ['PREVIOUS_ALIGNED', 'PREVIOUS_CALENDAR_PERIOD'] },
|
|
15680
17377
|
},
|
|
15681
17378
|
};
|
|
@@ -15693,6 +17390,16 @@ const chartDocumentSchema = {
|
|
|
15693
17390
|
},
|
|
15694
17391
|
},
|
|
15695
17392
|
},
|
|
17393
|
+
{
|
|
17394
|
+
if: { properties: { kind: { const: 'heatmap' } }, required: ['kind'] },
|
|
17395
|
+
then: {
|
|
17396
|
+
required: ['dimensions', 'metrics'],
|
|
17397
|
+
properties: {
|
|
17398
|
+
dimensions: { type: 'array', minItems: 2, maxItems: 2 },
|
|
17399
|
+
metrics: { type: 'array', minItems: 1, maxItems: 1 },
|
|
17400
|
+
},
|
|
17401
|
+
},
|
|
17402
|
+
},
|
|
15696
17403
|
{
|
|
15697
17404
|
if: { properties: { kind: { const: 'gauge' } }, required: ['kind'] },
|
|
15698
17405
|
then: {
|
|
@@ -15761,6 +17468,10 @@ const chartDocumentSchema = {
|
|
|
15761
17468
|
options: {
|
|
15762
17469
|
type: 'object',
|
|
15763
17470
|
properties: {
|
|
17471
|
+
timezone: { type: 'string' },
|
|
17472
|
+
periodField: { type: 'string' },
|
|
17473
|
+
periodPreset: { enum: chartComparisonPeriodPresets },
|
|
17474
|
+
fillGaps: { type: 'boolean' },
|
|
15764
17475
|
comparisonPeriod: chartComparisonPeriodSchema,
|
|
15765
17476
|
},
|
|
15766
17477
|
},
|
|
@@ -15882,8 +17593,9 @@ const PRAXIS_CHARTS_AUTHORING_MANIFEST = {
|
|
|
15882
17593
|
required: ['field'],
|
|
15883
17594
|
properties: {
|
|
15884
17595
|
field: { type: 'string' },
|
|
17596
|
+
sourceField: { type: 'string' },
|
|
15885
17597
|
label: { oneOf: [{ type: 'string' }, { type: 'object' }] },
|
|
15886
|
-
aggregation: { enum: ['sum', 'avg', 'min', 'max', 'count', 'distinct-count'] },
|
|
17598
|
+
aggregation: { enum: ['sum', 'avg', 'min', 'max', 'count', 'distinct-count', 'avg-per-cycle-occurrence'] },
|
|
15887
17599
|
seriesKind: { enum: ['bar', 'line', 'area'] },
|
|
15888
17600
|
axis: { enum: ['primary', 'secondary'] },
|
|
15889
17601
|
color: { type: 'string' },
|
|
@@ -15900,8 +17612,9 @@ const PRAXIS_CHARTS_AUTHORING_MANIFEST = {
|
|
|
15900
17612
|
required: ['field'],
|
|
15901
17613
|
properties: {
|
|
15902
17614
|
field: { type: 'string' },
|
|
17615
|
+
sourceField: { type: 'string' },
|
|
15903
17616
|
label: { oneOf: [{ type: 'string' }, { type: 'object' }] },
|
|
15904
|
-
aggregation: { enum: ['sum', 'avg', 'min', 'max', 'count', 'distinct-count'] },
|
|
17617
|
+
aggregation: { enum: ['sum', 'avg', 'min', 'max', 'count', 'distinct-count', 'avg-per-cycle-occurrence'] },
|
|
15905
17618
|
seriesKind: { enum: ['bar', 'line', 'area'] },
|
|
15906
17619
|
axis: { enum: ['primary', 'secondary'] },
|
|
15907
17620
|
color: { type: 'string' },
|
|
@@ -15980,8 +17693,8 @@ const PRAXIS_CHARTS_AUTHORING_MANIFEST = {
|
|
|
15980
17693
|
operation: { enum: chartStatsOperations },
|
|
15981
17694
|
limit: { type: 'integer', minimum: 1 },
|
|
15982
17695
|
comparisonPeriod: chartComparisonPeriodSchema,
|
|
15983
|
-
dimensions: { type: 'array', items: { type: 'object', required: ['field'], properties: { field: { type: 'string' }, role: { type: 'string' } } } },
|
|
15984
|
-
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' } } } },
|
|
17696
|
+
dimensions: { type: 'array', items: { type: 'object', required: ['field'], properties: { field: { type: 'string' }, role: { type: 'string' }, extraction: { type: 'string' }, alias: { type: 'string' } } } },
|
|
17697
|
+
metrics: { type: 'array', items: { type: 'object', required: ['field'], properties: { field: { type: 'string' }, sourceField: { type: 'string' }, label: { oneOf: [{ type: 'string' }, { type: 'object' }] }, aggregation: { enum: ['sum', 'avg', 'min', 'max', 'count', 'distinct-count', 'avg-per-cycle-occurrence'] }, seriesKind: { enum: ['bar', 'line', 'area'] }, axis: { enum: ['primary', 'secondary'] }, color: { type: 'string' }, format: { type: 'string' }, afterField: { type: 'string' } } } },
|
|
15985
17698
|
},
|
|
15986
17699
|
},
|
|
15987
17700
|
effects: [{ kind: 'compile-domain-patch', handler: 'chart-data-resource-bind', handlerContract: {
|
|
@@ -16353,4 +18066,4 @@ function createResourceStatsDataSource(config) {
|
|
|
16353
18066
|
* Generated bundle index. Do not edit.
|
|
16354
18067
|
*/
|
|
16355
18068
|
|
|
16356
|
-
export { AnalyticsChartConfigAdapterService, AnalyticsChartContractService, ChartContractNormalizerService, ChartContractValidationService, ChartEditorDefaultsService, ChartEditorPreviewMapperService, ChartResourceCapabilityCatalogAdapter, PRAXIS_ANALYTICS_PRESENTATION_AUTHORING_MANIFEST, PRAXIS_ANALYTICS_PRESENTATION_COMPONENT_METADATA, PRAXIS_CHARTS_AUTHORING_MANIFEST, PRAXIS_CHARTS_I18N, PRAXIS_CHART_AUTHORING_COVERAGE, PRAXIS_CHART_BACKEND_MOCK_BAR, PRAXIS_CHART_BACKEND_MOCK_COMBO, PRAXIS_CHART_BACKEND_MOCK_DONUT, PRAXIS_CHART_BACKEND_MOCK_HORIZONTAL_BAR, PRAXIS_CHART_BACKEND_MOCK_MULTI_METRIC_BAR, PRAXIS_CHART_BACKEND_MOCK_SCATTER, PRAXIS_CHART_BACKEND_MOCK_STACKED_AREA, PRAXIS_CHART_BACKEND_MOCK_TIMESERIES, PRAXIS_CHART_COMPONENT_METADATA, PRAXIS_CHART_DRILLDOWN_DATA_BY_MONTH, PRAXIS_CHART_DRILLDOWN_PANEL_METADATA, PRAXIS_CHART_ENGINE, PRAXIS_CHART_ENGINE_FACTORY, PRAXIS_CHART_PALETTE_TOKENS, PRAXIS_CHART_STATE_PROBE_COMPONENT_METADATA, PRAXIS_CHART_THEME_VARIANTS, PRAXIS_MICRO_VISUALIZATION_COMPONENT_METADATA, PRAXIS_X_UI_CHART_AUTHORABLE_VERSION, PraxisAnalyticsPresentationComponent, PraxisChartBackendPayloadAdapterService, PraxisChartCanonicalContractMapperService, PraxisChartComponent, PraxisChartCompositionShowcaseComponent, PraxisChartConfigEditor, PraxisChartDataTransformerService, PraxisChartDrilldownPanelComponent, PraxisChartSchemaMapperService, PraxisChartStateProbeComponent, PraxisChartStatsApiService, PraxisChartWidgetConfigEditor, PraxisMicroVisualizationComponent, buildPraxisChartInteractiveCanvasPage, buildPraxisChartInteractiveWidgetPage, buildPraxisChartMockCanvasPage, buildPraxisChartMockWidgetPage, createPraxisChartsI18nConfig, createResourceStatsDataSource, isPraxisChartPaletteToken, providePraxisAnalyticsPresentationMetadata, providePraxisChartDrilldownPanelMetadata, providePraxisChartStateProbeMetadata, providePraxisCharts, providePraxisChartsI18n, providePraxisChartsMetadata, providePraxisMicroVisualizationMetadata, resolvePraxisChartPaletteToken, resolvePraxisChartsText };
|
|
18069
|
+
export { AnalyticsChartConfigAdapterService, AnalyticsChartContractService, ChartContractNormalizerService, ChartContractValidationService, ChartEditorDefaultsService, ChartEditorPreviewMapperService, ChartResourceCapabilityCatalogAdapter, PRAXIS_ANALYTICS_PRESENTATION_AUTHORING_MANIFEST, PRAXIS_ANALYTICS_PRESENTATION_COMPONENT_METADATA, PRAXIS_CHARTS_AUTHORING_MANIFEST, PRAXIS_CHARTS_I18N, PRAXIS_CHART_AUTHORING_COVERAGE, PRAXIS_CHART_BACKEND_MOCK_BAR, PRAXIS_CHART_BACKEND_MOCK_COMBO, PRAXIS_CHART_BACKEND_MOCK_DONUT, PRAXIS_CHART_BACKEND_MOCK_HEATMAP_MATRIX, 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_MICRO_VISUALIZATION_COMPONENT_METADATA, PRAXIS_X_UI_CHART_AUTHORABLE_VERSION, PraxisAnalyticsPresentationComponent, PraxisChartBackendPayloadAdapterService, PraxisChartCanonicalContractMapperService, PraxisChartComponent, PraxisChartCompositionShowcaseComponent, PraxisChartConfigEditor, PraxisChartDataTransformerService, PraxisChartDrilldownPanelComponent, PraxisChartSchemaMapperService, PraxisChartStateProbeComponent, PraxisChartStatsApiService, PraxisChartWidgetConfigEditor, PraxisMicroVisualizationComponent, buildPraxisChartInteractiveCanvasPage, buildPraxisChartInteractiveWidgetPage, buildPraxisChartMockCanvasPage, buildPraxisChartMockWidgetPage, createPraxisChartsI18nConfig, createResourceStatsDataSource, isPraxisChartPaletteToken, providePraxisAnalyticsPresentationMetadata, providePraxisChartDrilldownPanelMetadata, providePraxisChartStateProbeMetadata, providePraxisCharts, providePraxisChartsI18n, providePraxisChartsMetadata, providePraxisMicroVisualizationMetadata, resolvePraxisChartPaletteToken, resolvePraxisChartsText };
|