@praxisui/charts 6.0.0-beta.0 → 8.0.0-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +90 -13
- package/fesm2022/praxisui-charts.mjs +714 -93
- package/index.d.ts +108 -8
- package/package.json +2 -2
|
@@ -255,6 +255,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
|
|
|
255
255
|
}] });
|
|
256
256
|
|
|
257
257
|
const PRAXIS_CHART_DEFAULT_PALETTE = ['#1263b4', '#0f766e', '#f08c00', '#c92a2a', '#7b61ff'];
|
|
258
|
+
const TITLE_TOP = 18;
|
|
259
|
+
const TITLE_LEFT = 24;
|
|
260
|
+
const CARTESIAN_GRID_TOP_WITH_TITLE = 96;
|
|
261
|
+
const CARTESIAN_GRID_TOP_WITHOUT_TITLE = 48;
|
|
262
|
+
const CARTESIAN_GRID_BOTTOM_WITH_LEGEND = 64;
|
|
263
|
+
const CARTESIAN_GRID_BOTTOM_WITHOUT_LEGEND = 40;
|
|
258
264
|
class PraxisChartOptionBuilderService {
|
|
259
265
|
transformer;
|
|
260
266
|
constructor(transformer) {
|
|
@@ -266,12 +272,21 @@ class PraxisChartOptionBuilderService {
|
|
|
266
272
|
const tooltipEnabled = config.theme?.tooltip?.enabled ?? true;
|
|
267
273
|
const legendVisible = config.theme?.legend?.visible ?? true;
|
|
268
274
|
if (transformed.mode === 'pie') {
|
|
275
|
+
const hasChartTitle = this.hasText(config.title) || this.hasText(config.subtitle);
|
|
276
|
+
const labelsVisible = config.series[0]?.labels?.visible ?? false;
|
|
269
277
|
const pieSeries = {
|
|
270
278
|
type: 'pie',
|
|
271
|
-
radius: config.type === 'donut'
|
|
279
|
+
radius: config.type === 'donut'
|
|
280
|
+
? labelsVisible ? ['32%', '52%'] : ['42%', '64%']
|
|
281
|
+
: labelsVisible ? '54%' : '66%',
|
|
282
|
+
center: ['50%', hasChartTitle ? '54%' : '50%'],
|
|
283
|
+
avoidLabelOverlap: true,
|
|
272
284
|
label: {
|
|
273
|
-
show:
|
|
285
|
+
show: labelsVisible,
|
|
286
|
+
overflow: 'truncate',
|
|
287
|
+
width: 120,
|
|
274
288
|
},
|
|
289
|
+
labelLine: labelsVisible ? { length: 14, length2: 12 } : undefined,
|
|
275
290
|
data: transformed.slices.map((slice) => ({
|
|
276
291
|
name: slice.name,
|
|
277
292
|
value: slice.value,
|
|
@@ -281,15 +296,9 @@ class PraxisChartOptionBuilderService {
|
|
|
281
296
|
return {
|
|
282
297
|
backgroundColor: config.theme?.backgroundColor,
|
|
283
298
|
color: palette,
|
|
284
|
-
title:
|
|
285
|
-
text: this.resolveText(config.title),
|
|
286
|
-
subtext: this.resolveText(config.subtitle),
|
|
287
|
-
left: 'center',
|
|
288
|
-
},
|
|
299
|
+
title: this.buildTitle(config, 'center'),
|
|
289
300
|
tooltip: tooltipEnabled ? { trigger: 'item' } : undefined,
|
|
290
|
-
legend: legendVisible
|
|
291
|
-
? { show: true, orient: 'horizontal', bottom: 0 }
|
|
292
|
-
: { show: false },
|
|
301
|
+
legend: this.buildLegend(config, legendVisible, 'bottom'),
|
|
293
302
|
series: [pieSeries],
|
|
294
303
|
};
|
|
295
304
|
}
|
|
@@ -305,29 +314,31 @@ class PraxisChartOptionBuilderService {
|
|
|
305
314
|
return {
|
|
306
315
|
backgroundColor: config.theme?.backgroundColor,
|
|
307
316
|
color: palette,
|
|
308
|
-
title:
|
|
309
|
-
text: this.resolveText(config.title),
|
|
310
|
-
subtext: this.resolveText(config.subtitle),
|
|
311
|
-
left: 'left',
|
|
312
|
-
},
|
|
317
|
+
title: this.buildTitle(config, 'left'),
|
|
313
318
|
tooltip: tooltipEnabled ? { trigger: 'item' } : undefined,
|
|
314
|
-
legend: legendVisible
|
|
315
|
-
grid: this.buildGrid(),
|
|
319
|
+
legend: this.buildLegend(config, legendVisible, 'bottom'),
|
|
320
|
+
grid: this.buildGrid(config, { legendVisible }),
|
|
316
321
|
xAxis: {
|
|
317
322
|
type: config.axes?.x?.type ?? 'value',
|
|
318
323
|
name: config.axes?.x?.label,
|
|
324
|
+
nameLocation: 'middle',
|
|
325
|
+
nameGap: 32,
|
|
319
326
|
axisLabel: {
|
|
320
327
|
show: config.axes?.x?.labels?.visible ?? true,
|
|
321
328
|
rotate: config.axes?.x?.labels?.rotate ?? 0,
|
|
329
|
+
hideOverlap: true,
|
|
322
330
|
},
|
|
323
331
|
},
|
|
324
332
|
yAxis: {
|
|
325
333
|
type: config.axes?.y?.type ?? 'value',
|
|
326
334
|
name: config.axes?.y?.label,
|
|
335
|
+
nameLocation: 'middle',
|
|
336
|
+
nameGap: 40,
|
|
327
337
|
min: config.axes?.y?.min,
|
|
328
338
|
max: config.axes?.y?.max,
|
|
329
339
|
axisLabel: {
|
|
330
340
|
show: config.axes?.y?.labels?.visible ?? true,
|
|
341
|
+
hideOverlap: true,
|
|
331
342
|
},
|
|
332
343
|
},
|
|
333
344
|
series: scatterSeries,
|
|
@@ -337,35 +348,39 @@ class PraxisChartOptionBuilderService {
|
|
|
337
348
|
return {
|
|
338
349
|
backgroundColor: config.theme?.backgroundColor,
|
|
339
350
|
color: palette,
|
|
340
|
-
title:
|
|
341
|
-
text: this.resolveText(config.title),
|
|
342
|
-
subtext: this.resolveText(config.subtitle),
|
|
343
|
-
left: 'left',
|
|
344
|
-
},
|
|
351
|
+
title: this.buildTitle(config, 'left'),
|
|
345
352
|
tooltip: tooltipEnabled
|
|
346
|
-
? {
|
|
353
|
+
? {
|
|
354
|
+
trigger: config.theme?.tooltip?.trigger ?? 'axis',
|
|
355
|
+
confine: true,
|
|
356
|
+
appendToBody: false,
|
|
357
|
+
}
|
|
347
358
|
: undefined,
|
|
348
|
-
legend: legendVisible
|
|
349
|
-
|
|
350
|
-
: { show: false },
|
|
351
|
-
grid: this.buildGrid(),
|
|
359
|
+
legend: this.buildLegend(config, legendVisible, 'bottom'),
|
|
360
|
+
grid: this.buildGrid(config, { horizontal, legendVisible }),
|
|
352
361
|
xAxis: horizontal
|
|
353
362
|
? {
|
|
354
363
|
type: config.axes?.y?.type ?? 'value',
|
|
355
364
|
name: config.axes?.y?.label,
|
|
365
|
+
nameLocation: 'middle',
|
|
366
|
+
nameGap: 32,
|
|
356
367
|
min: config.axes?.y?.min,
|
|
357
368
|
max: config.axes?.y?.max,
|
|
358
369
|
axisLabel: {
|
|
359
370
|
show: config.axes?.y?.labels?.visible ?? true,
|
|
371
|
+
hideOverlap: true,
|
|
360
372
|
},
|
|
361
373
|
}
|
|
362
374
|
: {
|
|
363
375
|
type: config.axes?.x?.type ?? 'category',
|
|
364
376
|
name: config.axes?.x?.label,
|
|
365
377
|
data: transformed.categories,
|
|
378
|
+
nameLocation: 'middle',
|
|
379
|
+
nameGap: 18,
|
|
366
380
|
axisLabel: {
|
|
367
381
|
show: config.axes?.x?.labels?.visible ?? true,
|
|
368
382
|
rotate: config.axes?.x?.labels?.rotate ?? 0,
|
|
383
|
+
hideOverlap: true,
|
|
369
384
|
},
|
|
370
385
|
},
|
|
371
386
|
yAxis: horizontal
|
|
@@ -376,6 +391,9 @@ class PraxisChartOptionBuilderService {
|
|
|
376
391
|
axisLabel: {
|
|
377
392
|
show: config.axes?.x?.labels?.visible ?? true,
|
|
378
393
|
rotate: config.axes?.x?.labels?.rotate ?? 0,
|
|
394
|
+
hideOverlap: true,
|
|
395
|
+
overflow: 'truncate',
|
|
396
|
+
width: 160,
|
|
379
397
|
},
|
|
380
398
|
}
|
|
381
399
|
: this.buildCartesianYAxis(config),
|
|
@@ -402,14 +420,97 @@ class PraxisChartOptionBuilderService {
|
|
|
402
420
|
const text = value.text;
|
|
403
421
|
return text || undefined;
|
|
404
422
|
}
|
|
423
|
+
if (typeof value === 'object' && value && 'fallback' in value) {
|
|
424
|
+
const fallback = value.fallback;
|
|
425
|
+
return fallback || undefined;
|
|
426
|
+
}
|
|
405
427
|
return undefined;
|
|
406
428
|
}
|
|
407
|
-
|
|
429
|
+
hasText(value) {
|
|
430
|
+
const text = this.resolveText(value);
|
|
431
|
+
return !!text && text.trim().length > 0;
|
|
432
|
+
}
|
|
433
|
+
buildTitle(config, align) {
|
|
434
|
+
const title = this.resolveText(config.title);
|
|
435
|
+
const subtitle = this.resolveText(config.subtitle);
|
|
436
|
+
const hasTitle = !!title || !!subtitle;
|
|
437
|
+
return {
|
|
438
|
+
show: hasTitle,
|
|
439
|
+
text: title,
|
|
440
|
+
subtext: subtitle,
|
|
441
|
+
top: TITLE_TOP,
|
|
442
|
+
left: align === 'center' ? 'center' : TITLE_LEFT,
|
|
443
|
+
right: TITLE_LEFT,
|
|
444
|
+
textStyle: {
|
|
445
|
+
fontSize: 18,
|
|
446
|
+
fontWeight: 600,
|
|
447
|
+
lineHeight: 24,
|
|
448
|
+
color: config.theme?.textColor,
|
|
449
|
+
overflow: 'truncate',
|
|
450
|
+
},
|
|
451
|
+
subtextStyle: {
|
|
452
|
+
fontSize: 12,
|
|
453
|
+
lineHeight: 18,
|
|
454
|
+
color: config.theme?.textColor,
|
|
455
|
+
overflow: 'truncate',
|
|
456
|
+
},
|
|
457
|
+
};
|
|
458
|
+
}
|
|
459
|
+
buildLegend(config, visible, fallbackPosition) {
|
|
460
|
+
if (!visible) {
|
|
461
|
+
return { show: false };
|
|
462
|
+
}
|
|
463
|
+
const position = config.theme?.legend?.position ?? fallbackPosition;
|
|
464
|
+
const base = {
|
|
465
|
+
show: true,
|
|
466
|
+
type: 'scroll',
|
|
467
|
+
pageIconSize: 10,
|
|
468
|
+
itemWidth: 18,
|
|
469
|
+
itemHeight: 10,
|
|
470
|
+
textStyle: {
|
|
471
|
+
color: config.theme?.textColor ?? '#4b5563',
|
|
472
|
+
},
|
|
473
|
+
formatter: (name) => truncateLegendText(name),
|
|
474
|
+
};
|
|
475
|
+
if (position === 'top') {
|
|
476
|
+
return {
|
|
477
|
+
...base,
|
|
478
|
+
orient: 'horizontal',
|
|
479
|
+
top: this.hasText(config.title) || this.hasText(config.subtitle) ? 70 : 12,
|
|
480
|
+
left: 'center',
|
|
481
|
+
right: TITLE_LEFT,
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
if (position === 'left' || position === 'right') {
|
|
485
|
+
return {
|
|
486
|
+
...base,
|
|
487
|
+
orient: 'vertical',
|
|
488
|
+
top: 88,
|
|
489
|
+
bottom: 28,
|
|
490
|
+
[position]: 10,
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
return {
|
|
494
|
+
...base,
|
|
495
|
+
orient: 'horizontal',
|
|
496
|
+
bottom: 16,
|
|
497
|
+
left: TITLE_LEFT,
|
|
498
|
+
right: TITLE_LEFT,
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
buildGrid(config, options = {}) {
|
|
502
|
+
const hasChartTitle = this.hasText(config.title) || this.hasText(config.subtitle);
|
|
503
|
+
const legendPosition = config.theme?.legend?.position ?? 'bottom';
|
|
504
|
+
const sideLegend = options.legendVisible && (legendPosition === 'left' || legendPosition === 'right');
|
|
408
505
|
return {
|
|
409
|
-
top:
|
|
410
|
-
right:
|
|
411
|
-
bottom:
|
|
412
|
-
|
|
506
|
+
top: hasChartTitle ? CARTESIAN_GRID_TOP_WITH_TITLE : CARTESIAN_GRID_TOP_WITHOUT_TITLE,
|
|
507
|
+
right: sideLegend && legendPosition === 'right' ? 160 : 32,
|
|
508
|
+
bottom: options.legendVisible && legendPosition === 'bottom'
|
|
509
|
+
? CARTESIAN_GRID_BOTTOM_WITH_LEGEND
|
|
510
|
+
: CARTESIAN_GRID_BOTTOM_WITHOUT_LEGEND,
|
|
511
|
+
left: sideLegend && legendPosition === 'left'
|
|
512
|
+
? 168
|
|
513
|
+
: options.horizontal ? 136 : 56,
|
|
413
514
|
// ECharts deprecou containLabel; esta combinacao preserva o mesmo comportamento
|
|
414
515
|
// sem depender da feature legacy de grid.
|
|
415
516
|
outerBoundsMode: 'same',
|
|
@@ -420,10 +521,13 @@ class PraxisChartOptionBuilderService {
|
|
|
420
521
|
const primaryAxis = {
|
|
421
522
|
type: config.axes?.y?.type ?? 'value',
|
|
422
523
|
name: config.axes?.y?.label,
|
|
524
|
+
nameLocation: 'middle',
|
|
525
|
+
nameGap: 40,
|
|
423
526
|
min: config.axes?.y?.min,
|
|
424
527
|
max: config.axes?.y?.max,
|
|
425
528
|
axisLabel: {
|
|
426
529
|
show: config.axes?.y?.labels?.visible ?? true,
|
|
530
|
+
hideOverlap: true,
|
|
427
531
|
},
|
|
428
532
|
};
|
|
429
533
|
if (!config.axes?.ySecondary) {
|
|
@@ -434,11 +538,14 @@ class PraxisChartOptionBuilderService {
|
|
|
434
538
|
{
|
|
435
539
|
type: config.axes.ySecondary.type ?? 'value',
|
|
436
540
|
name: config.axes.ySecondary.label,
|
|
541
|
+
nameLocation: 'middle',
|
|
542
|
+
nameGap: 40,
|
|
437
543
|
min: config.axes.ySecondary.min,
|
|
438
544
|
max: config.axes.ySecondary.max,
|
|
439
545
|
position: config.axes.ySecondary.position ?? 'right',
|
|
440
546
|
axisLabel: {
|
|
441
547
|
show: config.axes.ySecondary.labels?.visible ?? true,
|
|
548
|
+
hideOverlap: true,
|
|
442
549
|
},
|
|
443
550
|
},
|
|
444
551
|
];
|
|
@@ -450,6 +557,13 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
|
|
|
450
557
|
type: Injectable,
|
|
451
558
|
args: [{ providedIn: 'root' }]
|
|
452
559
|
}], ctorParameters: () => [{ type: PraxisChartDataTransformerService }] });
|
|
560
|
+
function truncateLegendText(name) {
|
|
561
|
+
const value = String(name ?? '').trim();
|
|
562
|
+
if (value.length <= 28) {
|
|
563
|
+
return value;
|
|
564
|
+
}
|
|
565
|
+
return `${value.slice(0, 25)}...`;
|
|
566
|
+
}
|
|
453
567
|
|
|
454
568
|
use([
|
|
455
569
|
AriaComponent,
|
|
@@ -503,10 +617,59 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
|
|
|
503
617
|
type: Injectable
|
|
504
618
|
}], ctorParameters: () => [{ type: PraxisChartOptionBuilderService }] });
|
|
505
619
|
|
|
620
|
+
const PRAXIS_CHART_PALETTE_TOKENS = {
|
|
621
|
+
'brand-primary': ['#1263b4', '#0f766e', '#f08c00', '#c92a2a', '#7b61ff'],
|
|
622
|
+
'brand-balanced': ['#1263b4', '#15803d', '#7c3aed', '#c2410c', '#be123c'],
|
|
623
|
+
status: ['#15803d', '#ca8a04', '#dc2626', '#2563eb', '#7c3aed'],
|
|
624
|
+
executive: ['#14b8a6', '#f59e0b', '#e11d48', '#8b5cf6', '#38bdf8'],
|
|
625
|
+
};
|
|
626
|
+
function isPraxisChartPaletteToken(value) {
|
|
627
|
+
return Object.prototype.hasOwnProperty.call(PRAXIS_CHART_PALETTE_TOKENS, value);
|
|
628
|
+
}
|
|
629
|
+
function resolvePraxisChartPaletteToken(value) {
|
|
630
|
+
return isPraxisChartPaletteToken(value)
|
|
631
|
+
? [...PRAXIS_CHART_PALETTE_TOKENS[value]]
|
|
632
|
+
: undefined;
|
|
633
|
+
}
|
|
634
|
+
const PRAXIS_CHART_THEME_VARIANTS = {
|
|
635
|
+
default: {
|
|
636
|
+
palette: [...PRAXIS_CHART_PALETTE_TOKENS['brand-primary']],
|
|
637
|
+
legend: { position: 'bottom' },
|
|
638
|
+
surface: { mode: 'auto' },
|
|
639
|
+
},
|
|
640
|
+
compact: {
|
|
641
|
+
palette: [...PRAXIS_CHART_PALETTE_TOKENS['brand-balanced']],
|
|
642
|
+
textColor: '#374151',
|
|
643
|
+
borderRadius: 6,
|
|
644
|
+
legend: { position: 'bottom' },
|
|
645
|
+
surface: {
|
|
646
|
+
mode: 'embedded',
|
|
647
|
+
background: 'transparent',
|
|
648
|
+
borderWidth: 0,
|
|
649
|
+
borderRadius: 0,
|
|
650
|
+
},
|
|
651
|
+
},
|
|
652
|
+
executive: {
|
|
653
|
+
palette: [...PRAXIS_CHART_PALETTE_TOKENS.executive],
|
|
654
|
+
backgroundColor: '#111827',
|
|
655
|
+
textColor: '#f9fafb',
|
|
656
|
+
borderRadius: 8,
|
|
657
|
+
legend: { position: 'bottom' },
|
|
658
|
+
surface: {
|
|
659
|
+
mode: 'contained',
|
|
660
|
+
background: '#111827',
|
|
661
|
+
borderColor: '#374151',
|
|
662
|
+
borderWidth: 1,
|
|
663
|
+
borderRadius: 8,
|
|
664
|
+
},
|
|
665
|
+
},
|
|
666
|
+
};
|
|
667
|
+
|
|
506
668
|
class ChartContractNormalizerService {
|
|
507
669
|
normalize(input) {
|
|
508
670
|
let document = structuredClone(input);
|
|
509
671
|
document = this.normalizeMotion(document);
|
|
672
|
+
document = this.normalizeSizing(document);
|
|
510
673
|
document = this.normalizeKindSpecificFields(document);
|
|
511
674
|
document = this.normalizeOperationSpecificFields(document);
|
|
512
675
|
document = this.normalizeSourceSpecificFields(document);
|
|
@@ -532,6 +695,44 @@ class ChartContractNormalizerService {
|
|
|
532
695
|
},
|
|
533
696
|
};
|
|
534
697
|
}
|
|
698
|
+
normalizeSizing(document) {
|
|
699
|
+
if (!document.sizing && document.height !== undefined) {
|
|
700
|
+
const { height, ...rest } = document;
|
|
701
|
+
return {
|
|
702
|
+
...rest,
|
|
703
|
+
sizing: {
|
|
704
|
+
mode: 'fixed',
|
|
705
|
+
height: this.normalizeCssSizeValue(height),
|
|
706
|
+
},
|
|
707
|
+
};
|
|
708
|
+
}
|
|
709
|
+
if (!document.sizing) {
|
|
710
|
+
return document;
|
|
711
|
+
}
|
|
712
|
+
const mode = document.sizing.mode ?? (document.sizing.height !== undefined ? 'fixed' : 'auto');
|
|
713
|
+
const { height: sizingHeight, ...restSizing } = document.sizing;
|
|
714
|
+
const normalizedSizing = {
|
|
715
|
+
...restSizing,
|
|
716
|
+
minHeight: this.normalizeCssSizeValue(restSizing.minHeight),
|
|
717
|
+
maxHeight: this.normalizeCssSizeValue(restSizing.maxHeight),
|
|
718
|
+
aspectRatio: this.normalizeAspectRatioValue(restSizing.aspectRatio),
|
|
719
|
+
};
|
|
720
|
+
const sizing = mode === 'fixed'
|
|
721
|
+
? {
|
|
722
|
+
...normalizedSizing,
|
|
723
|
+
mode,
|
|
724
|
+
height: this.normalizeCssSizeValue(sizingHeight),
|
|
725
|
+
}
|
|
726
|
+
: {
|
|
727
|
+
...normalizedSizing,
|
|
728
|
+
mode,
|
|
729
|
+
};
|
|
730
|
+
const { height, ...rest } = document;
|
|
731
|
+
return {
|
|
732
|
+
...rest,
|
|
733
|
+
sizing: this.omitUndefined(sizing),
|
|
734
|
+
};
|
|
735
|
+
}
|
|
535
736
|
normalizeKindSpecificFields(document) {
|
|
536
737
|
let nextDocument = document;
|
|
537
738
|
if (document.kind === 'horizontal-bar') {
|
|
@@ -594,6 +795,29 @@ class ChartContractNormalizerService {
|
|
|
594
795
|
isDistribution(document) {
|
|
595
796
|
return document.source.kind === 'praxis.stats' && document.source.operation === 'distribution';
|
|
596
797
|
}
|
|
798
|
+
normalizeCssSizeValue(value) {
|
|
799
|
+
if (typeof value !== 'string') {
|
|
800
|
+
return value;
|
|
801
|
+
}
|
|
802
|
+
const trimmed = value.trim();
|
|
803
|
+
if (!trimmed) {
|
|
804
|
+
return undefined;
|
|
805
|
+
}
|
|
806
|
+
return /^-?\d+(\.\d+)?$/.test(trimmed) ? Number(trimmed) : trimmed;
|
|
807
|
+
}
|
|
808
|
+
normalizeAspectRatioValue(value) {
|
|
809
|
+
if (typeof value !== 'string') {
|
|
810
|
+
return value;
|
|
811
|
+
}
|
|
812
|
+
const trimmed = value.trim();
|
|
813
|
+
if (!trimmed) {
|
|
814
|
+
return undefined;
|
|
815
|
+
}
|
|
816
|
+
return /^\d+(\.\d+)?$/.test(trimmed) ? Number(trimmed) : trimmed;
|
|
817
|
+
}
|
|
818
|
+
omitUndefined(value) {
|
|
819
|
+
return Object.fromEntries(Object.entries(value).filter(([, entryValue]) => entryValue !== undefined));
|
|
820
|
+
}
|
|
597
821
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: ChartContractNormalizerService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
598
822
|
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: ChartContractNormalizerService, providedIn: 'root' });
|
|
599
823
|
}
|
|
@@ -607,6 +831,7 @@ class ChartContractValidationService {
|
|
|
607
831
|
const issues = [];
|
|
608
832
|
this.validateSource(document, issues);
|
|
609
833
|
this.validateTheme(document, issues);
|
|
834
|
+
this.validateSizing(document, issues);
|
|
610
835
|
this.validateMetrics(document, issues);
|
|
611
836
|
this.validateKinds(document, issues);
|
|
612
837
|
this.validateEvents(document, issues);
|
|
@@ -628,34 +853,59 @@ class ChartContractValidationService {
|
|
|
628
853
|
}
|
|
629
854
|
validateTheme(document, issues) {
|
|
630
855
|
if (document.theme?.palette && typeof document.theme.palette === 'string') {
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
issues.push(this.error('theme-variant-unsupported', 'theme.variant', 'x-ui.chart theme.variant is not yet implemented in @praxisui/charts.'));
|
|
856
|
+
if (!isPraxisChartPaletteToken(document.theme.palette)) {
|
|
857
|
+
issues.push(this.error('palette-token-unknown', 'theme.palette', `x-ui.chart theme.palette token "${document.theme.palette}" is not registered in @praxisui/charts.`));
|
|
858
|
+
}
|
|
635
859
|
}
|
|
636
860
|
}
|
|
637
861
|
validateMetrics(document, issues) {
|
|
638
862
|
if (!document.metrics?.length) {
|
|
639
|
-
issues.push(this.error('missing-metric', 'metrics', 'x-ui.chart requires at least one metric
|
|
863
|
+
issues.push(this.error('missing-metric', 'metrics', 'x-ui.chart requires at least one metric.'));
|
|
640
864
|
}
|
|
641
865
|
const aggregations = [
|
|
642
866
|
...(document.metrics?.map((metric) => metric.aggregation).filter(Boolean) ?? []),
|
|
643
867
|
...(document.aggregations?.map((aggregation) => aggregation.operation) ?? []),
|
|
644
868
|
];
|
|
645
869
|
if (aggregations.includes('distinct-count')) {
|
|
646
|
-
issues.push(this.error('distinct-count-unsupported', 'metrics', 'x-ui.chart aggregation "distinct-count" is not
|
|
870
|
+
issues.push(this.error('distinct-count-unsupported', 'metrics', 'x-ui.chart aggregation "distinct-count" is not supported in @praxisui/charts.'));
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
validateSizing(document, issues) {
|
|
874
|
+
const sizing = document.sizing;
|
|
875
|
+
if (!sizing) {
|
|
876
|
+
return;
|
|
877
|
+
}
|
|
878
|
+
if (sizing.mode
|
|
879
|
+
&& sizing.mode !== 'fixed'
|
|
880
|
+
&& sizing.mode !== 'fill-container'
|
|
881
|
+
&& sizing.mode !== 'auto') {
|
|
882
|
+
issues.push(this.error('sizing-mode-unsupported', 'sizing.mode', `x-ui.chart sizing.mode="${sizing.mode}" is not supported in @praxisui/charts.`));
|
|
883
|
+
}
|
|
884
|
+
if (sizing.mode === 'fixed' && sizing.height === undefined) {
|
|
885
|
+
issues.push(this.error('sizing-fixed-missing-height', 'sizing.height', 'x-ui.chart sizing.height is required when sizing.mode="fixed".'));
|
|
886
|
+
}
|
|
887
|
+
this.validateCssSize('height', sizing.height, 'sizing.height', issues);
|
|
888
|
+
this.validateCssSize('minHeight', sizing.minHeight, 'sizing.minHeight', issues);
|
|
889
|
+
this.validateCssSize('maxHeight', sizing.maxHeight, 'sizing.maxHeight', issues);
|
|
890
|
+
const minHeight = this.numericCssSize(sizing.minHeight);
|
|
891
|
+
const maxHeight = this.numericCssSize(sizing.maxHeight);
|
|
892
|
+
if (minHeight !== null && maxHeight !== null && maxHeight < minHeight) {
|
|
893
|
+
issues.push(this.error('sizing-max-height-before-min-height', 'sizing.maxHeight', 'x-ui.chart sizing.maxHeight must be greater than or equal to sizing.minHeight.'));
|
|
894
|
+
}
|
|
895
|
+
if (sizing.aspectRatio !== undefined && !this.isValidAspectRatio(sizing.aspectRatio)) {
|
|
896
|
+
issues.push(this.error('sizing-aspect-ratio-invalid', 'sizing.aspectRatio', 'x-ui.chart sizing.aspectRatio must be a positive number or a CSS aspect-ratio value such as "16 / 9".'));
|
|
647
897
|
}
|
|
648
898
|
}
|
|
649
899
|
validateKinds(document, issues) {
|
|
650
900
|
const metricCount = document.metrics?.length ?? 0;
|
|
651
901
|
if (document.kind !== 'pie' && document.kind !== 'donut' && !document.dimensions?.length) {
|
|
652
|
-
issues.push(this.error('missing-dimension', 'dimensions', 'x-ui.chart cartesian charts require at least one dimension
|
|
902
|
+
issues.push(this.error('missing-dimension', 'dimensions', 'x-ui.chart cartesian charts require at least one dimension.'));
|
|
653
903
|
}
|
|
654
904
|
if ((document.kind === 'pie' || document.kind === 'donut') && !document.dimensions?.[0]?.field) {
|
|
655
905
|
issues.push(this.error('pie-missing-dimension', 'dimensions[0].field', 'x-ui.chart pie/donut charts require a first dimension for category mapping.'));
|
|
656
906
|
}
|
|
657
907
|
if ((document.kind === 'pie' || document.kind === 'donut') && metricCount > 1) {
|
|
658
|
-
issues.push(this.error('pie-multi-metric', 'metrics', 'x-ui.chart pie/donut charts
|
|
908
|
+
issues.push(this.error('pie-multi-metric', 'metrics', 'x-ui.chart pie/donut charts support a single metric in @praxisui/charts.'));
|
|
659
909
|
}
|
|
660
910
|
if (document.kind === 'combo' && metricCount < 2) {
|
|
661
911
|
issues.push(this.error('combo-min-metrics', 'metrics', 'x-ui.chart combo charts require at least two metrics.'));
|
|
@@ -666,7 +916,7 @@ class ChartContractValidationService {
|
|
|
666
916
|
if (document.source.kind === 'praxis.stats'
|
|
667
917
|
&& document.source.operation === 'distribution'
|
|
668
918
|
&& metricCount > 1) {
|
|
669
|
-
issues.push(this.error('distribution-single-metric', 'metrics', 'x-ui.chart praxis.stats distribution
|
|
919
|
+
issues.push(this.error('distribution-single-metric', 'metrics', 'x-ui.chart praxis.stats distribution supports only a single metric in @praxisui/charts.'));
|
|
670
920
|
}
|
|
671
921
|
if (document.kind === 'horizontal-bar' && document.orientation && document.orientation !== 'horizontal') {
|
|
672
922
|
issues.push(this.error('horizontal-bar-orientation', 'orientation', 'x-ui.chart kind="horizontal-bar" requires orientation="horizontal" when orientation is provided.'));
|
|
@@ -681,17 +931,17 @@ class ChartContractValidationService {
|
|
|
681
931
|
&& document.source.kind === 'praxis.stats'
|
|
682
932
|
&& document.source.operation !== 'group-by'
|
|
683
933
|
&& document.source.operation !== 'timeseries') {
|
|
684
|
-
issues.push(this.error('combo-operation-unsupported', 'source.operation', 'x-ui.chart combo charts over praxis.stats
|
|
934
|
+
issues.push(this.error('combo-operation-unsupported', 'source.operation', 'x-ui.chart combo charts over praxis.stats support only group-by or timeseries operations in @praxisui/charts.'));
|
|
685
935
|
}
|
|
686
936
|
}
|
|
687
937
|
validateEvents(document, issues) {
|
|
688
938
|
this.validateEventAction('pointClick', document.events?.pointClick, issues);
|
|
689
939
|
this.validateEventAction('drillDown', document.events?.drillDown, issues);
|
|
690
940
|
if (document.events?.selectionChange) {
|
|
691
|
-
issues.push(this.error('selection-change-unsupported', 'events.selectionChange', 'x-ui.chart selectionChange/crossFilter declarative runtime actions are not
|
|
941
|
+
issues.push(this.error('selection-change-unsupported', 'events.selectionChange', 'x-ui.chart selectionChange/crossFilter declarative runtime actions are not supported in @praxisui/charts.'));
|
|
692
942
|
}
|
|
693
943
|
if (document.events?.crossFilter) {
|
|
694
|
-
issues.push(this.error('cross-filter-unsupported', 'events.crossFilter', 'x-ui.chart selectionChange/crossFilter declarative runtime actions are not
|
|
944
|
+
issues.push(this.error('cross-filter-unsupported', 'events.crossFilter', 'x-ui.chart selectionChange/crossFilter declarative runtime actions are not supported in @praxisui/charts.'));
|
|
695
945
|
}
|
|
696
946
|
}
|
|
697
947
|
validateEventAction(eventKey, eventAction, issues) {
|
|
@@ -710,6 +960,43 @@ class ChartContractValidationService {
|
|
|
710
960
|
message,
|
|
711
961
|
};
|
|
712
962
|
}
|
|
963
|
+
validateCssSize(key, value, field, issues) {
|
|
964
|
+
if (value === undefined) {
|
|
965
|
+
return;
|
|
966
|
+
}
|
|
967
|
+
if (!this.isValidCssSize(value)) {
|
|
968
|
+
issues.push(this.error(`sizing-${key}-invalid`, field, `x-ui.chart ${field} must be a positive number or a CSS length such as "320px", "20rem", "50%" or "calc(100% - 16px)".`));
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
isValidCssSize(value) {
|
|
972
|
+
if (typeof value === 'number') {
|
|
973
|
+
return Number.isFinite(value) && value >= 0;
|
|
974
|
+
}
|
|
975
|
+
const trimmed = value.trim();
|
|
976
|
+
return trimmed.length > 0 && (/^\d+(\.\d+)?$/.test(trimmed)
|
|
977
|
+
|| /^\d+(\.\d+)?(px|rem|em|vh|vw|vmin|vmax|%)$/.test(trimmed)
|
|
978
|
+
|| /^(calc|min|max|clamp)\(.+\)$/.test(trimmed)
|
|
979
|
+
|| /^var\(.+\)$/.test(trimmed));
|
|
980
|
+
}
|
|
981
|
+
numericCssSize(value) {
|
|
982
|
+
if (typeof value === 'number') {
|
|
983
|
+
return Number.isFinite(value) ? value : null;
|
|
984
|
+
}
|
|
985
|
+
const trimmed = value?.trim();
|
|
986
|
+
if (!trimmed) {
|
|
987
|
+
return null;
|
|
988
|
+
}
|
|
989
|
+
const match = /^(\d+(\.\d+)?)(px)?$/.exec(trimmed);
|
|
990
|
+
return match ? Number(match[1]) : null;
|
|
991
|
+
}
|
|
992
|
+
isValidAspectRatio(value) {
|
|
993
|
+
if (typeof value === 'number') {
|
|
994
|
+
return Number.isFinite(value) && value > 0;
|
|
995
|
+
}
|
|
996
|
+
const trimmed = value.trim();
|
|
997
|
+
return (/^\d+(\.\d+)?$/.test(trimmed)
|
|
998
|
+
|| /^\d+(\.\d+)?\s*\/\s*\d+(\.\d+)?$/.test(trimmed));
|
|
999
|
+
}
|
|
713
1000
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: ChartContractValidationService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
714
1001
|
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: ChartContractValidationService, providedIn: 'root' });
|
|
715
1002
|
}
|
|
@@ -738,7 +1025,7 @@ class PraxisChartCanonicalContractMapperService {
|
|
|
738
1025
|
orientation: this.resolveOrientation(normalizedContract),
|
|
739
1026
|
title: this.mapTextValue(normalizedContract.title),
|
|
740
1027
|
subtitle: this.mapTextValue(normalizedContract.subtitle),
|
|
741
|
-
|
|
1028
|
+
sizing: normalizedContract.sizing,
|
|
742
1029
|
axes: this.buildAxes(normalizedContract),
|
|
743
1030
|
series: this.buildSeries(normalizedContract),
|
|
744
1031
|
dataSource: this.buildDataSource(normalizedContract),
|
|
@@ -877,15 +1164,38 @@ class PraxisChartCanonicalContractMapperService {
|
|
|
877
1164
|
};
|
|
878
1165
|
}
|
|
879
1166
|
buildTheme(contract) {
|
|
1167
|
+
const variantTheme = contract.theme?.variant
|
|
1168
|
+
? PRAXIS_CHART_THEME_VARIANTS[contract.theme.variant]
|
|
1169
|
+
: undefined;
|
|
1170
|
+
const palette = this.resolveThemePalette(contract);
|
|
880
1171
|
return {
|
|
881
|
-
|
|
882
|
-
|
|
1172
|
+
...variantTheme,
|
|
1173
|
+
palette: palette ?? variantTheme?.palette,
|
|
1174
|
+
legend: {
|
|
1175
|
+
...(variantTheme?.legend ?? {}),
|
|
1176
|
+
visible: this.resolveToggle(contract.legend, true),
|
|
1177
|
+
},
|
|
883
1178
|
tooltip: {
|
|
1179
|
+
...(variantTheme?.tooltip ?? {}),
|
|
884
1180
|
enabled: this.resolveToggle(contract.tooltip, true),
|
|
885
1181
|
trigger: contract.kind === 'pie' || contract.kind === 'donut' || contract.kind === 'scatter' ? 'item' : 'axis',
|
|
886
1182
|
},
|
|
1183
|
+
surface: {
|
|
1184
|
+
...(variantTheme?.surface ?? {}),
|
|
1185
|
+
...(contract.theme?.surface ?? {}),
|
|
1186
|
+
},
|
|
1187
|
+
backgroundColor: contract.theme?.surface?.background ?? variantTheme?.backgroundColor,
|
|
1188
|
+
textColor: variantTheme?.textColor,
|
|
1189
|
+
borderRadius: variantTheme?.borderRadius,
|
|
887
1190
|
};
|
|
888
1191
|
}
|
|
1192
|
+
resolveThemePalette(contract) {
|
|
1193
|
+
const palette = contract.theme?.palette;
|
|
1194
|
+
if (Array.isArray(palette)) {
|
|
1195
|
+
return palette;
|
|
1196
|
+
}
|
|
1197
|
+
return palette ? resolvePraxisChartPaletteToken(palette) : undefined;
|
|
1198
|
+
}
|
|
889
1199
|
buildMotion(motion) {
|
|
890
1200
|
if (!motion) {
|
|
891
1201
|
return undefined;
|
|
@@ -948,7 +1258,7 @@ class PraxisChartCanonicalContractMapperService {
|
|
|
948
1258
|
case undefined:
|
|
949
1259
|
return undefined;
|
|
950
1260
|
default:
|
|
951
|
-
throw new Error(`x-ui.chart aggregation "${aggregation}" is not
|
|
1261
|
+
throw new Error(`x-ui.chart aggregation "${aggregation}" is not supported in @praxisui/charts.`);
|
|
952
1262
|
}
|
|
953
1263
|
}
|
|
954
1264
|
toQueryFilterMap(filters) {
|
|
@@ -1114,7 +1424,7 @@ class PraxisChartCanonicalContractMapperService {
|
|
|
1114
1424
|
case 'max':
|
|
1115
1425
|
return 'MAX';
|
|
1116
1426
|
default:
|
|
1117
|
-
throw new Error(`x-ui.chart aggregation "${aggregation}" is not
|
|
1427
|
+
throw new Error(`x-ui.chart aggregation "${aggregation}" is not supported in @praxisui/charts.`);
|
|
1118
1428
|
}
|
|
1119
1429
|
}
|
|
1120
1430
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: PraxisChartCanonicalContractMapperService, deps: [{ token: ChartContractNormalizerService }, { token: ChartContractValidationService }], target: i0.ɵɵFactoryTarget.Injectable });
|
|
@@ -1330,6 +1640,7 @@ class PraxisChartComponent {
|
|
|
1330
1640
|
mappedRuntimeConfig = signal(null, ...(ngDevMode ? [{ debugName: "mappedRuntimeConfig" }] : []));
|
|
1331
1641
|
chartDocumentMappingError = signal(null, ...(ngDevMode ? [{ debugName: "chartDocumentMappingError" }] : []));
|
|
1332
1642
|
fillContainerHeight = signal(false, ...(ngDevMode ? [{ debugName: "fillContainerHeight" }] : []));
|
|
1643
|
+
resizeFrameId = null;
|
|
1333
1644
|
previousRemoteSignature = null;
|
|
1334
1645
|
previousDocumentSignature = null;
|
|
1335
1646
|
editorSessionSubscriptions = [];
|
|
@@ -1362,14 +1673,37 @@ class PraxisChartComponent {
|
|
|
1362
1673
|
return [];
|
|
1363
1674
|
}, ...(ngDevMode ? [{ debugName: "resolvedData" }] : []));
|
|
1364
1675
|
resolvedHeight = computed(() => {
|
|
1365
|
-
|
|
1676
|
+
const sizing = this.effectiveConfig().sizing;
|
|
1677
|
+
const mode = sizing?.mode ?? (sizing?.height !== undefined ? 'fixed' : undefined);
|
|
1678
|
+
if (this.fillContainerHeight() || mode === 'fill-container') {
|
|
1366
1679
|
return '100%';
|
|
1367
1680
|
}
|
|
1681
|
+
if (mode === 'auto') {
|
|
1682
|
+
return 'auto';
|
|
1683
|
+
}
|
|
1684
|
+
const sizingHeight = this.toCssSize(sizing?.height);
|
|
1685
|
+
if (sizingHeight) {
|
|
1686
|
+
return sizingHeight;
|
|
1687
|
+
}
|
|
1368
1688
|
const value = this.effectiveConfig().height;
|
|
1369
1689
|
if (typeof value === 'number')
|
|
1370
1690
|
return `${value}px`;
|
|
1371
1691
|
return value || 'var(--praxis-chart-runtime-height, 320px)';
|
|
1372
1692
|
}, ...(ngDevMode ? [{ debugName: "resolvedHeight" }] : []));
|
|
1693
|
+
resolvedMinHeight = computed(() => {
|
|
1694
|
+
const sizing = this.effectiveConfig().sizing;
|
|
1695
|
+
return this.toCssSize(sizing?.minHeight) ?? (this.isFillContainerMode() ? '0' : null);
|
|
1696
|
+
}, ...(ngDevMode ? [{ debugName: "resolvedMinHeight" }] : []));
|
|
1697
|
+
resolvedMaxHeight = computed(() => this.toCssSize(this.effectiveConfig().sizing?.maxHeight) ?? null, ...(ngDevMode ? [{ debugName: "resolvedMaxHeight" }] : []));
|
|
1698
|
+
resolvedAspectRatio = computed(() => this.toCssAspectRatio(this.effectiveConfig().sizing?.aspectRatio), ...(ngDevMode ? [{ debugName: "resolvedAspectRatio" }] : []));
|
|
1699
|
+
isFillContainerMode = computed(() => {
|
|
1700
|
+
return this.fillContainerHeight() || this.effectiveConfig().sizing?.mode === 'fill-container';
|
|
1701
|
+
}, ...(ngDevMode ? [{ debugName: "isFillContainerMode" }] : []));
|
|
1702
|
+
surfaceMode = computed(() => this.effectiveConfig().theme?.surface?.mode ?? 'auto', ...(ngDevMode ? [{ debugName: "surfaceMode" }] : []));
|
|
1703
|
+
surfaceBackground = computed(() => this.effectiveConfig().theme?.surface?.background ?? null, ...(ngDevMode ? [{ debugName: "surfaceBackground" }] : []));
|
|
1704
|
+
surfaceBorderColor = computed(() => this.effectiveConfig().theme?.surface?.borderColor ?? null, ...(ngDevMode ? [{ debugName: "surfaceBorderColor" }] : []));
|
|
1705
|
+
surfaceBorderWidth = computed(() => this.toCssSize(this.effectiveConfig().theme?.surface?.borderWidth) ?? null, ...(ngDevMode ? [{ debugName: "surfaceBorderWidth" }] : []));
|
|
1706
|
+
surfaceBorderRadius = computed(() => this.toCssSize(this.effectiveConfig().theme?.surface?.borderRadius) ?? null, ...(ngDevMode ? [{ debugName: "surfaceBorderRadius" }] : []));
|
|
1373
1707
|
renderConfig = computed(() => {
|
|
1374
1708
|
const config = this.effectiveConfig();
|
|
1375
1709
|
const explicitData = this.data();
|
|
@@ -1558,6 +1892,7 @@ class PraxisChartComponent {
|
|
|
1558
1892
|
this.clearEditorSessionSubscriptions();
|
|
1559
1893
|
this.resizeObserver()?.disconnect();
|
|
1560
1894
|
this.shellObserver()?.disconnect();
|
|
1895
|
+
this.cancelScheduledResize();
|
|
1561
1896
|
this.engine.destroy();
|
|
1562
1897
|
});
|
|
1563
1898
|
}
|
|
@@ -1615,11 +1950,28 @@ class PraxisChartComponent {
|
|
|
1615
1950
|
return;
|
|
1616
1951
|
}
|
|
1617
1952
|
const observer = new ResizeObserver(() => {
|
|
1618
|
-
this.
|
|
1953
|
+
this.scheduleEngineResize();
|
|
1619
1954
|
});
|
|
1620
1955
|
observer.observe(host);
|
|
1621
1956
|
this.resizeObserver.set(observer);
|
|
1622
1957
|
}
|
|
1958
|
+
toCssSize(value) {
|
|
1959
|
+
if (typeof value === 'number') {
|
|
1960
|
+
return `${value}px`;
|
|
1961
|
+
}
|
|
1962
|
+
const trimmed = value?.trim();
|
|
1963
|
+
if (trimmed && /^-?\d+(\.\d+)?$/.test(trimmed)) {
|
|
1964
|
+
return `${trimmed}px`;
|
|
1965
|
+
}
|
|
1966
|
+
return trimmed || null;
|
|
1967
|
+
}
|
|
1968
|
+
toCssAspectRatio(value) {
|
|
1969
|
+
if (typeof value === 'number') {
|
|
1970
|
+
return Number.isFinite(value) && value > 0 ? String(value) : null;
|
|
1971
|
+
}
|
|
1972
|
+
const trimmed = value?.trim();
|
|
1973
|
+
return trimmed || null;
|
|
1974
|
+
}
|
|
1623
1975
|
buildRemoteSignature(config) {
|
|
1624
1976
|
if (config.dataSource?.kind !== 'remote') {
|
|
1625
1977
|
return null;
|
|
@@ -1658,21 +2010,29 @@ class PraxisChartComponent {
|
|
|
1658
2010
|
this.shellObserver.set(observer);
|
|
1659
2011
|
}
|
|
1660
2012
|
scheduleResizeAfterShellModeChange() {
|
|
1661
|
-
|
|
2013
|
+
this.scheduleEngineResize();
|
|
2014
|
+
}
|
|
2015
|
+
scheduleEngineResize() {
|
|
2016
|
+
if (typeof requestAnimationFrame !== 'function') {
|
|
1662
2017
|
this.engine.resize();
|
|
1663
|
-
};
|
|
1664
|
-
if (typeof requestAnimationFrame === 'function') {
|
|
1665
|
-
requestAnimationFrame(() => {
|
|
1666
|
-
runResize();
|
|
1667
|
-
requestAnimationFrame(() => {
|
|
1668
|
-
runResize();
|
|
1669
|
-
});
|
|
1670
|
-
});
|
|
1671
2018
|
return;
|
|
1672
2019
|
}
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
}
|
|
2020
|
+
if (this.resizeFrameId !== null) {
|
|
2021
|
+
return;
|
|
2022
|
+
}
|
|
2023
|
+
this.resizeFrameId = requestAnimationFrame(() => {
|
|
2024
|
+
this.resizeFrameId = null;
|
|
2025
|
+
this.engine.resize();
|
|
2026
|
+
});
|
|
2027
|
+
}
|
|
2028
|
+
cancelScheduledResize() {
|
|
2029
|
+
if (this.resizeFrameId === null) {
|
|
2030
|
+
return;
|
|
2031
|
+
}
|
|
2032
|
+
if (typeof cancelAnimationFrame === 'function') {
|
|
2033
|
+
cancelAnimationFrame(this.resizeFrameId);
|
|
2034
|
+
}
|
|
2035
|
+
this.resizeFrameId = null;
|
|
1676
2036
|
}
|
|
1677
2037
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: PraxisChartComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1678
2038
|
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.17", type: PraxisChartComponent, isStandalone: true, selector: "praxis-chart", inputs: { config: { classPropertyName: "config", publicName: "config", isSignal: true, isRequired: true, transformFunction: null }, data: { classPropertyName: "data", publicName: "data", isSignal: true, isRequired: false, transformFunction: null }, chartDocument: { classPropertyName: "chartDocument", publicName: "chartDocument", isSignal: true, isRequired: false, transformFunction: null }, filterCriteria: { classPropertyName: "filterCriteria", publicName: "filterCriteria", isSignal: true, isRequired: false, transformFunction: null }, queryContext: { classPropertyName: "queryContext", publicName: "queryContext", 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", queryRequest: "queryRequest", loadStateChange: "loadStateChange", chartDocumentApplied: "chartDocumentApplied", chartDocumentSaved: "chartDocumentSaved" }, providers: [
|
|
@@ -1682,7 +2042,20 @@ class PraxisChartComponent {
|
|
|
1682
2042
|
useExisting: EChartsEngineAdapter,
|
|
1683
2043
|
},
|
|
1684
2044
|
], viewQueries: [{ propertyName: "chartHost", first: true, predicate: ["chartHost"], descendants: true, isSignal: true }], ngImport: i0, template: `
|
|
1685
|
-
<section
|
|
2045
|
+
<section
|
|
2046
|
+
class="praxis-chart-shell"
|
|
2047
|
+
[class.praxis-chart-shell-fill-container]="isFillContainerMode()"
|
|
2048
|
+
[class.praxis-chart-shell-contained]="surfaceMode() === 'contained'"
|
|
2049
|
+
[class.praxis-chart-shell-embedded]="surfaceMode() === 'embedded'"
|
|
2050
|
+
[style.height]="resolvedHeight()"
|
|
2051
|
+
[style.minHeight]="resolvedMinHeight()"
|
|
2052
|
+
[style.maxHeight]="resolvedMaxHeight()"
|
|
2053
|
+
[style.aspectRatio]="resolvedAspectRatio()"
|
|
2054
|
+
[style.--praxis-chart-config-surface-bg]="surfaceBackground()"
|
|
2055
|
+
[style.--praxis-chart-config-surface-border]="surfaceBorderColor()"
|
|
2056
|
+
[style.--praxis-chart-config-surface-border-width]="surfaceBorderWidth()"
|
|
2057
|
+
[style.--praxis-chart-config-surface-radius]="surfaceBorderRadius()"
|
|
2058
|
+
>
|
|
1686
2059
|
@if (canOpenConfigEditor()) {
|
|
1687
2060
|
<button
|
|
1688
2061
|
class="praxis-chart-settings-trigger"
|
|
@@ -1698,10 +2071,27 @@ class PraxisChartComponent {
|
|
|
1698
2071
|
|
|
1699
2072
|
@if (loadState() === 'loading') {
|
|
1700
2073
|
<div class="praxis-chart-state praxis-chart-state-loading">
|
|
1701
|
-
<div class="praxis-chart-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
2074
|
+
<div class="praxis-chart-loading-hero" aria-hidden="true">
|
|
2075
|
+
<div class="praxis-chart-loading-summary">
|
|
2076
|
+
<span class="praxis-chart-loading-chip"></span>
|
|
2077
|
+
<span class="praxis-chart-loading-chip praxis-chart-loading-chip--short"></span>
|
|
2078
|
+
</div>
|
|
2079
|
+
<div class="praxis-chart-loading-plot">
|
|
2080
|
+
<span class="praxis-chart-loading-bar praxis-chart-loading-bar--1"></span>
|
|
2081
|
+
<span class="praxis-chart-loading-bar praxis-chart-loading-bar--2"></span>
|
|
2082
|
+
<span class="praxis-chart-loading-bar praxis-chart-loading-bar--3"></span>
|
|
2083
|
+
<span class="praxis-chart-loading-bar praxis-chart-loading-bar--4"></span>
|
|
2084
|
+
<span class="praxis-chart-loading-bar praxis-chart-loading-bar--5"></span>
|
|
2085
|
+
</div>
|
|
2086
|
+
</div>
|
|
2087
|
+
<div class="praxis-chart-state-copy">
|
|
2088
|
+
@if (loadingLabel()) {
|
|
2089
|
+
<div class="praxis-chart-state-title">{{ loadingLabel() }}</div>
|
|
2090
|
+
}
|
|
2091
|
+
<div class="praxis-chart-state-description">
|
|
2092
|
+
Publicando o recorte analitico e preparando os buckets visuais.
|
|
2093
|
+
</div>
|
|
2094
|
+
</div>
|
|
1705
2095
|
</div>
|
|
1706
2096
|
} @else if (loadState() === 'error') {
|
|
1707
2097
|
<div class="praxis-chart-state praxis-chart-state-error">
|
|
@@ -1725,7 +2115,7 @@ class PraxisChartComponent {
|
|
|
1725
2115
|
<div #chartHost class="praxis-chart-host"></div>
|
|
1726
2116
|
}
|
|
1727
2117
|
</section>
|
|
1728
|
-
`, isInline: true, styles: [":host{display:block;height:100%;min-width:0}:host-context(.pdx-shell.expanded) .praxis-chart-shell,:host-context(.pdx-shell.fullscreen) .praxis-chart-shell{height:100%!important}.praxis-chart-shell{position:relative;width:100%;height:100%;min-height:240px;border-radius:
|
|
2118
|
+
`, isInline: true, styles: [":host{display:block;height:100%;min-width:0}:host-context(.pdx-shell.expanded) .praxis-chart-shell,:host-context(.pdx-shell.fullscreen) .praxis-chart-shell{height:100%!important}.praxis-chart-shell{position:relative;width:100%;height:100%;min-height:240px;border-radius:var(--praxis-chart-config-surface-radius, var(--praxis-chart-surface-radius, 8px));overflow:hidden;background:var( --praxis-chart-config-surface-bg, var(--praxis-chart-surface-bg, var(--md-sys-color-surface-container-lowest, #fff)) );border-color:var( --praxis-chart-config-surface-border, var( --praxis-chart-surface-border, color-mix(in srgb, var(--md-sys-color-outline, #c5c7ce) 44%, transparent) ) );border-style:solid;border-width:var(--praxis-chart-config-surface-border-width, 1px)}.praxis-chart-shell-fill-container{min-height:0}:host-context(.pdx-shell) .praxis-chart-shell:not(.praxis-chart-shell-contained),.praxis-chart-shell-embedded{border-width:var( --praxis-chart-config-surface-border-width, var(--praxis-chart-embedded-surface-border-width, 0) );border-radius:var( --praxis-chart-config-surface-radius, var(--praxis-chart-embedded-surface-radius, 0) );background:var( --praxis-chart-config-surface-bg, var(--praxis-chart-embedded-surface-bg, transparent) )}.praxis-chart-settings-trigger{position:absolute;top:10px;right:10px;z-index:3;background:color-mix(in srgb,var(--md-sys-color-surface, #fff) 88%,rgba(18,99,180,.12));-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px)}.praxis-chart-host{width:100%;height:100%}.praxis-chart-state{height:100%;min-height:min(240px,100%);display:grid;align-content:center;justify-items:center;gap:14px;padding:24px;text-align:center}.praxis-chart-state-copy{display:grid;gap:6px;justify-items:center}.praxis-chart-state-title{font-size:1rem;font-weight:600;color:var(--md-sys-color-on-surface, #1a1b20)}.praxis-chart-state-description{font-size:.925rem;color:var(--md-sys-color-on-surface-variant, #5a5d67);max-width:36rem}.praxis-chart-loading-hero{width:min(100%,320px);display:grid;gap:14px}.praxis-chart-loading-summary{display:flex;gap:8px;justify-content:center}.praxis-chart-loading-chip,.praxis-chart-loading-bar{border-radius:999px;background:linear-gradient(90deg,#1263b414,#1263b438,#1263b414);background-size:200% 100%;animation:praxis-chart-loading-wave 1.2s ease-in-out infinite}.praxis-chart-loading-chip{display:block;width:104px;height:12px}.praxis-chart-loading-chip--short{width:64px}.praxis-chart-loading-plot{display:grid;grid-template-columns:repeat(5,minmax(0,1fr));align-items:end;gap:10px;height:108px;padding:12px 8px 4px;border-radius:8px;background:color-mix(in srgb,var(--md-sys-color-surface-container, #eef3f8) 72%,transparent);border:1px solid rgba(18,99,180,.08)}.praxis-chart-loading-bar{display:block;width:100%}.praxis-chart-loading-bar--1{height:32%}.praxis-chart-loading-bar--2{height:68%}.praxis-chart-loading-bar--3{height:48%}.praxis-chart-loading-bar--4{height:78%}.praxis-chart-loading-bar--5{height:56%}@keyframes praxis-chart-loading-wave{0%{background-position:100% 0}to{background-position:-100% 0}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i1$1.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i2.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatTooltipModule }, { kind: "directive", type: i3.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
1729
2119
|
}
|
|
1730
2120
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: PraxisChartComponent, decorators: [{
|
|
1731
2121
|
type: Component,
|
|
@@ -1736,7 +2126,20 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
|
|
|
1736
2126
|
useExisting: EChartsEngineAdapter,
|
|
1737
2127
|
},
|
|
1738
2128
|
], changeDetection: ChangeDetectionStrategy.OnPush, template: `
|
|
1739
|
-
<section
|
|
2129
|
+
<section
|
|
2130
|
+
class="praxis-chart-shell"
|
|
2131
|
+
[class.praxis-chart-shell-fill-container]="isFillContainerMode()"
|
|
2132
|
+
[class.praxis-chart-shell-contained]="surfaceMode() === 'contained'"
|
|
2133
|
+
[class.praxis-chart-shell-embedded]="surfaceMode() === 'embedded'"
|
|
2134
|
+
[style.height]="resolvedHeight()"
|
|
2135
|
+
[style.minHeight]="resolvedMinHeight()"
|
|
2136
|
+
[style.maxHeight]="resolvedMaxHeight()"
|
|
2137
|
+
[style.aspectRatio]="resolvedAspectRatio()"
|
|
2138
|
+
[style.--praxis-chart-config-surface-bg]="surfaceBackground()"
|
|
2139
|
+
[style.--praxis-chart-config-surface-border]="surfaceBorderColor()"
|
|
2140
|
+
[style.--praxis-chart-config-surface-border-width]="surfaceBorderWidth()"
|
|
2141
|
+
[style.--praxis-chart-config-surface-radius]="surfaceBorderRadius()"
|
|
2142
|
+
>
|
|
1740
2143
|
@if (canOpenConfigEditor()) {
|
|
1741
2144
|
<button
|
|
1742
2145
|
class="praxis-chart-settings-trigger"
|
|
@@ -1752,10 +2155,27 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
|
|
|
1752
2155
|
|
|
1753
2156
|
@if (loadState() === 'loading') {
|
|
1754
2157
|
<div class="praxis-chart-state praxis-chart-state-loading">
|
|
1755
|
-
<div class="praxis-chart-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
2158
|
+
<div class="praxis-chart-loading-hero" aria-hidden="true">
|
|
2159
|
+
<div class="praxis-chart-loading-summary">
|
|
2160
|
+
<span class="praxis-chart-loading-chip"></span>
|
|
2161
|
+
<span class="praxis-chart-loading-chip praxis-chart-loading-chip--short"></span>
|
|
2162
|
+
</div>
|
|
2163
|
+
<div class="praxis-chart-loading-plot">
|
|
2164
|
+
<span class="praxis-chart-loading-bar praxis-chart-loading-bar--1"></span>
|
|
2165
|
+
<span class="praxis-chart-loading-bar praxis-chart-loading-bar--2"></span>
|
|
2166
|
+
<span class="praxis-chart-loading-bar praxis-chart-loading-bar--3"></span>
|
|
2167
|
+
<span class="praxis-chart-loading-bar praxis-chart-loading-bar--4"></span>
|
|
2168
|
+
<span class="praxis-chart-loading-bar praxis-chart-loading-bar--5"></span>
|
|
2169
|
+
</div>
|
|
2170
|
+
</div>
|
|
2171
|
+
<div class="praxis-chart-state-copy">
|
|
2172
|
+
@if (loadingLabel()) {
|
|
2173
|
+
<div class="praxis-chart-state-title">{{ loadingLabel() }}</div>
|
|
2174
|
+
}
|
|
2175
|
+
<div class="praxis-chart-state-description">
|
|
2176
|
+
Publicando o recorte analitico e preparando os buckets visuais.
|
|
2177
|
+
</div>
|
|
2178
|
+
</div>
|
|
1759
2179
|
</div>
|
|
1760
2180
|
} @else if (loadState() === 'error') {
|
|
1761
2181
|
<div class="praxis-chart-state praxis-chart-state-error">
|
|
@@ -1779,7 +2199,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
|
|
|
1779
2199
|
<div #chartHost class="praxis-chart-host"></div>
|
|
1780
2200
|
}
|
|
1781
2201
|
</section>
|
|
1782
|
-
`, styles: [":host{display:block;height:100%;min-width:0}:host-context(.pdx-shell.expanded) .praxis-chart-shell,:host-context(.pdx-shell.fullscreen) .praxis-chart-shell{height:100%!important}.praxis-chart-shell{position:relative;width:100%;height:100%;min-height:240px;border-radius:
|
|
2202
|
+
`, styles: [":host{display:block;height:100%;min-width:0}:host-context(.pdx-shell.expanded) .praxis-chart-shell,:host-context(.pdx-shell.fullscreen) .praxis-chart-shell{height:100%!important}.praxis-chart-shell{position:relative;width:100%;height:100%;min-height:240px;border-radius:var(--praxis-chart-config-surface-radius, var(--praxis-chart-surface-radius, 8px));overflow:hidden;background:var( --praxis-chart-config-surface-bg, var(--praxis-chart-surface-bg, var(--md-sys-color-surface-container-lowest, #fff)) );border-color:var( --praxis-chart-config-surface-border, var( --praxis-chart-surface-border, color-mix(in srgb, var(--md-sys-color-outline, #c5c7ce) 44%, transparent) ) );border-style:solid;border-width:var(--praxis-chart-config-surface-border-width, 1px)}.praxis-chart-shell-fill-container{min-height:0}:host-context(.pdx-shell) .praxis-chart-shell:not(.praxis-chart-shell-contained),.praxis-chart-shell-embedded{border-width:var( --praxis-chart-config-surface-border-width, var(--praxis-chart-embedded-surface-border-width, 0) );border-radius:var( --praxis-chart-config-surface-radius, var(--praxis-chart-embedded-surface-radius, 0) );background:var( --praxis-chart-config-surface-bg, var(--praxis-chart-embedded-surface-bg, transparent) )}.praxis-chart-settings-trigger{position:absolute;top:10px;right:10px;z-index:3;background:color-mix(in srgb,var(--md-sys-color-surface, #fff) 88%,rgba(18,99,180,.12));-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px)}.praxis-chart-host{width:100%;height:100%}.praxis-chart-state{height:100%;min-height:min(240px,100%);display:grid;align-content:center;justify-items:center;gap:14px;padding:24px;text-align:center}.praxis-chart-state-copy{display:grid;gap:6px;justify-items:center}.praxis-chart-state-title{font-size:1rem;font-weight:600;color:var(--md-sys-color-on-surface, #1a1b20)}.praxis-chart-state-description{font-size:.925rem;color:var(--md-sys-color-on-surface-variant, #5a5d67);max-width:36rem}.praxis-chart-loading-hero{width:min(100%,320px);display:grid;gap:14px}.praxis-chart-loading-summary{display:flex;gap:8px;justify-content:center}.praxis-chart-loading-chip,.praxis-chart-loading-bar{border-radius:999px;background:linear-gradient(90deg,#1263b414,#1263b438,#1263b414);background-size:200% 100%;animation:praxis-chart-loading-wave 1.2s ease-in-out infinite}.praxis-chart-loading-chip{display:block;width:104px;height:12px}.praxis-chart-loading-chip--short{width:64px}.praxis-chart-loading-plot{display:grid;grid-template-columns:repeat(5,minmax(0,1fr));align-items:end;gap:10px;height:108px;padding:12px 8px 4px;border-radius:8px;background:color-mix(in srgb,var(--md-sys-color-surface-container, #eef3f8) 72%,transparent);border:1px solid rgba(18,99,180,.08)}.praxis-chart-loading-bar{display:block;width:100%}.praxis-chart-loading-bar--1{height:32%}.praxis-chart-loading-bar--2{height:68%}.praxis-chart-loading-bar--3{height:48%}.praxis-chart-loading-bar--4{height:78%}.praxis-chart-loading-bar--5{height:56%}@keyframes praxis-chart-loading-wave{0%{background-position:100% 0}to{background-position:-100% 0}}\n"] }]
|
|
1783
2203
|
}], ctorParameters: () => [], propDecorators: { config: [{ type: i0.Input, args: [{ isSignal: true, alias: "config", required: true }] }], data: [{ type: i0.Input, args: [{ isSignal: true, alias: "data", required: false }] }], chartDocument: [{ type: i0.Input, args: [{ isSignal: true, alias: "chartDocument", required: false }] }], filterCriteria: [{ type: i0.Input, args: [{ isSignal: true, alias: "filterCriteria", required: false }] }], queryContext: [{ type: i0.Input, args: [{ isSignal: true, alias: "queryContext", 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"] }], queryRequest: [{ type: i0.Output, args: ["queryRequest"] }], loadStateChange: [{ type: i0.Output, args: ["loadStateChange"] }], chartDocumentApplied: [{ type: i0.Output, args: ["chartDocumentApplied"] }], chartDocumentSaved: [{ type: i0.Output, args: ["chartDocumentSaved"] }], chartHost: [{ type: i0.ViewChild, args: ['chartHost', { isSignal: true }] }] } });
|
|
1784
2204
|
function normalizeFilterCriteria(criteria) {
|
|
1785
2205
|
if (!criteria)
|
|
@@ -3086,7 +3506,7 @@ const PRAXIS_CHART_BACKEND_MOCK_BAR = {
|
|
|
3086
3506
|
chartId: 'monthly-revenue-chart',
|
|
3087
3507
|
title: { key: 'charts.heroes.universe.title', fallback: 'Heroes por universo' },
|
|
3088
3508
|
subtitle: { key: 'charts.heroes.universe.subtitle', fallback: 'Group-by em /vw-perfil-heroi' },
|
|
3089
|
-
height: 320,
|
|
3509
|
+
sizing: { mode: 'fixed', height: 320 },
|
|
3090
3510
|
source: {
|
|
3091
3511
|
kind: 'praxis.stats',
|
|
3092
3512
|
resource: 'api/human-resources/vw-perfil-heroi',
|
|
@@ -3276,7 +3696,7 @@ const PRAXIS_CHART_BACKEND_MOCK_HORIZONTAL_BAR = {
|
|
|
3276
3696
|
title: { key: 'charts.payroll.department.title', fallback: 'Massa salarial por departamento' },
|
|
3277
3697
|
subtitle: { key: 'charts.payroll.department.subtitle', fallback: 'Ranking horizontal consumindo praxis.stats' },
|
|
3278
3698
|
orientation: 'horizontal',
|
|
3279
|
-
height: 340,
|
|
3699
|
+
sizing: { mode: 'fixed', height: 340 },
|
|
3280
3700
|
source: {
|
|
3281
3701
|
kind: 'praxis.stats',
|
|
3282
3702
|
resource: 'api/human-resources/vw-analytics-folha-pagamento',
|
|
@@ -3337,7 +3757,7 @@ const PRAXIS_CHART_BACKEND_MOCK_STACKED_AREA = {
|
|
|
3337
3757
|
chartId: 'payroll-net-stacked-area',
|
|
3338
3758
|
title: { key: 'charts.payroll.stackedArea.title', fallback: 'Composicao temporal da folha' },
|
|
3339
3759
|
subtitle: { key: 'charts.payroll.stackedArea.subtitle', fallback: 'Stacked area sobre payroll analytics' },
|
|
3340
|
-
height: 340,
|
|
3760
|
+
sizing: { mode: 'fixed', height: 340 },
|
|
3341
3761
|
source: {
|
|
3342
3762
|
kind: 'praxis.stats',
|
|
3343
3763
|
resource: 'api/human-resources/vw-analytics-folha-pagamento',
|
|
@@ -3396,7 +3816,7 @@ const PRAXIS_CHART_BACKEND_MOCK_MULTI_METRIC_BAR = {
|
|
|
3396
3816
|
key: 'charts.payroll.multiMetric.subtitle',
|
|
3397
3817
|
fallback: 'Massa liquida e desconto medio via group-by sem eixo secundario',
|
|
3398
3818
|
},
|
|
3399
|
-
height: 340,
|
|
3819
|
+
sizing: { mode: 'fixed', height: 340 },
|
|
3400
3820
|
source: {
|
|
3401
3821
|
kind: 'praxis.stats',
|
|
3402
3822
|
resource: 'api/human-resources/vw-analytics-folha-pagamento',
|
|
@@ -3459,7 +3879,7 @@ const PRAXIS_CHART_BACKEND_MOCK_SCATTER = {
|
|
|
3459
3879
|
chartId: 'payroll-net-vs-discount-scatter',
|
|
3460
3880
|
title: { key: 'charts.payroll.scatter.title', fallback: 'Liquido x desconto medio' },
|
|
3461
3881
|
subtitle: { key: 'charts.payroll.scatter.subtitle', fallback: 'Scatter sobre recortes do payroll' },
|
|
3462
|
-
height: 340,
|
|
3882
|
+
sizing: { mode: 'fixed', height: 340 },
|
|
3463
3883
|
source: {
|
|
3464
3884
|
kind: 'praxis.stats',
|
|
3465
3885
|
resource: 'api/human-resources/vw-analytics-folha-pagamento',
|
|
@@ -3515,7 +3935,7 @@ const PRAXIS_CHART_BACKEND_MOCK_COMBO = {
|
|
|
3515
3935
|
chartId: 'payroll-combo-variance',
|
|
3516
3936
|
title: { key: 'charts.payroll.combo.title', fallback: 'Folha liquida e desconto medio' },
|
|
3517
3937
|
subtitle: { key: 'charts.payroll.combo.subtitle', fallback: 'Barras para liquido e linha para desconto medio via praxis.stats' },
|
|
3518
|
-
height: 340,
|
|
3938
|
+
sizing: { mode: 'fixed', height: 340 },
|
|
3519
3939
|
source: {
|
|
3520
3940
|
kind: 'praxis.stats',
|
|
3521
3941
|
resource: 'api/human-resources/vw-analytics-folha-pagamento',
|
|
@@ -4235,7 +4655,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
|
|
|
4235
4655
|
const PRAXIS_CHARTS_EN_US = {
|
|
4236
4656
|
'praxis.charts.runtime.editChart': 'Edit chart settings',
|
|
4237
4657
|
'praxis.charts.runtime.invalidDocumentTitle': 'Invalid canonical configuration',
|
|
4238
|
-
'praxis.charts.runtime.invalidDocumentDescription': 'The canonical chart document could not be mapped
|
|
4658
|
+
'praxis.charts.runtime.invalidDocumentDescription': 'The canonical chart document could not be mapped by Praxis Charts. Review the chart contract before continuing.',
|
|
4239
4659
|
'praxis.charts.editor.section.general': 'General',
|
|
4240
4660
|
'praxis.charts.editor.section.data': 'Data',
|
|
4241
4661
|
'praxis.charts.editor.section.analytics': 'Analytics',
|
|
@@ -4247,7 +4667,11 @@ const PRAXIS_CHARTS_EN_US = {
|
|
|
4247
4667
|
'praxis.charts.editor.field.kind': 'Kind',
|
|
4248
4668
|
'praxis.charts.editor.field.title': 'Title',
|
|
4249
4669
|
'praxis.charts.editor.field.subtitle': 'Subtitle',
|
|
4670
|
+
'praxis.charts.editor.field.sizingMode': 'Sizing',
|
|
4250
4671
|
'praxis.charts.editor.field.height': 'Height',
|
|
4672
|
+
'praxis.charts.editor.field.minHeight': 'Minimum height',
|
|
4673
|
+
'praxis.charts.editor.field.maxHeight': 'Maximum height',
|
|
4674
|
+
'praxis.charts.editor.field.aspectRatio': 'Aspect ratio',
|
|
4251
4675
|
'praxis.charts.editor.field.sourceKind': 'Source',
|
|
4252
4676
|
'praxis.charts.editor.field.resource': 'Resource',
|
|
4253
4677
|
'praxis.charts.editor.field.operation': 'Operation',
|
|
@@ -4266,7 +4690,11 @@ const PRAXIS_CHARTS_EN_US = {
|
|
|
4266
4690
|
'praxis.charts.editor.field.legendEnabled': 'Show legend',
|
|
4267
4691
|
'praxis.charts.editor.field.labelsEnabled': 'Show labels',
|
|
4268
4692
|
'praxis.charts.editor.field.tooltipEnabled': 'Show tooltip',
|
|
4693
|
+
'praxis.charts.editor.field.themeVariant': 'Theme variant',
|
|
4694
|
+
'praxis.charts.editor.field.paletteMode': 'Palette mode',
|
|
4695
|
+
'praxis.charts.editor.field.paletteToken': 'Palette token',
|
|
4269
4696
|
'praxis.charts.editor.field.palette': 'Palette colors',
|
|
4697
|
+
'praxis.charts.editor.field.surfaceMode': 'Surface mode',
|
|
4270
4698
|
'praxis.charts.editor.field.emptyTitle': 'Empty title',
|
|
4271
4699
|
'praxis.charts.editor.field.emptyDescription': 'Empty description',
|
|
4272
4700
|
'praxis.charts.editor.field.loadingTitle': 'Loading title',
|
|
@@ -4285,8 +4713,15 @@ const PRAXIS_CHARTS_EN_US = {
|
|
|
4285
4713
|
'praxis.charts.editor.issues.empty': 'No issues were identified.',
|
|
4286
4714
|
'praxis.charts.editor.appearance.featuresTitle': 'Display features',
|
|
4287
4715
|
'praxis.charts.editor.appearance.paletteTitle': 'Palette',
|
|
4288
|
-
'praxis.charts.editor.appearance.paletteHint': 'Use
|
|
4716
|
+
'praxis.charts.editor.appearance.paletteHint': 'Use a registered token or comma-separated colors to persist theme.palette in the canonical contract.',
|
|
4717
|
+
'praxis.charts.editor.appearance.surfaceTitle': 'Surface',
|
|
4718
|
+
'praxis.charts.editor.appearance.surfaceHint': 'Use embedded for dashboard widgets and contained only when the chart must own its visual surface.',
|
|
4289
4719
|
'praxis.charts.editor.appearance.statesTitle': 'State messages',
|
|
4720
|
+
'praxis.charts.editor.hint.sizingMode': 'Use fill-container only when the host widget provides a defined body height.',
|
|
4721
|
+
'praxis.charts.editor.hint.height': 'Numbers are saved as pixels; CSS lengths such as 20rem are also accepted.',
|
|
4722
|
+
'praxis.charts.editor.hint.minHeight': 'Set a readable minimum for compact dashboard widgets.',
|
|
4723
|
+
'praxis.charts.editor.hint.maxHeight': 'Leave empty unless the chart must stop growing inside a flexible layout.',
|
|
4724
|
+
'praxis.charts.editor.hint.aspectRatio': 'Optional. Use values such as 1.777 or 16 / 9.',
|
|
4290
4725
|
'praxis.charts.editor.analytics.dimensionsTitle': 'Dimensions',
|
|
4291
4726
|
'praxis.charts.editor.analytics.metricsTitle': 'Metrics',
|
|
4292
4727
|
'praxis.charts.editor.analytics.addDimension': 'Add dimension',
|
|
@@ -4298,6 +4733,16 @@ const PRAXIS_CHARTS_EN_US = {
|
|
|
4298
4733
|
'praxis.charts.editor.specialization.comboTitle': 'Combo guidance',
|
|
4299
4734
|
'praxis.charts.editor.specialization.comboHint': 'Combo charts require at least two metrics and allow per-metric axis and series kind mapping.',
|
|
4300
4735
|
'praxis.charts.editor.specialization.pieDonutTitle': 'Composition guidance',
|
|
4736
|
+
'praxis.charts.editor.themeVariant.none': 'No variant',
|
|
4737
|
+
'praxis.charts.editor.themeVariant.default': 'Default',
|
|
4738
|
+
'praxis.charts.editor.themeVariant.compact': 'Compact',
|
|
4739
|
+
'praxis.charts.editor.themeVariant.executive': 'Executive',
|
|
4740
|
+
'praxis.charts.editor.paletteMode.token': 'Registered token',
|
|
4741
|
+
'praxis.charts.editor.paletteMode.custom': 'Custom colors',
|
|
4742
|
+
'praxis.charts.editor.paletteToken.brand-primary': 'Brand primary',
|
|
4743
|
+
'praxis.charts.editor.paletteToken.brand-balanced': 'Brand balanced',
|
|
4744
|
+
'praxis.charts.editor.paletteToken.status': 'Status',
|
|
4745
|
+
'praxis.charts.editor.paletteToken.executive': 'Executive',
|
|
4301
4746
|
'praxis.charts.editor.specialization.pieDonutHint': 'Pie and donut charts keep only the first metric and use the first dimension as the category segment.',
|
|
4302
4747
|
'praxis.charts.editor.specialization.scatterTitle': 'Scatter guidance',
|
|
4303
4748
|
'praxis.charts.editor.specialization.scatterHint': 'Scatter charts use the first dimension as X and the first metric as Y.',
|
|
@@ -4306,6 +4751,12 @@ const PRAXIS_CHARTS_EN_US = {
|
|
|
4306
4751
|
'praxis.charts.editor.events.none': 'None',
|
|
4307
4752
|
'praxis.charts.editor.sourceKind.praxisStats': 'praxis.stats',
|
|
4308
4753
|
'praxis.charts.editor.sourceKind.derived': 'derived',
|
|
4754
|
+
'praxis.charts.editor.sizing.fixed': 'Fixed height',
|
|
4755
|
+
'praxis.charts.editor.sizing.fill-container': 'Fill widget',
|
|
4756
|
+
'praxis.charts.editor.sizing.auto': 'Automatic',
|
|
4757
|
+
'praxis.charts.editor.surface.auto': 'Automatic',
|
|
4758
|
+
'praxis.charts.editor.surface.embedded': 'Embedded in widget',
|
|
4759
|
+
'praxis.charts.editor.surface.contained': 'Contained surface',
|
|
4309
4760
|
'praxis.charts.editor.operation.groupBy': 'group-by',
|
|
4310
4761
|
'praxis.charts.editor.operation.timeseries': 'timeseries',
|
|
4311
4762
|
'praxis.charts.editor.operation.distribution': 'distribution',
|
|
@@ -4355,7 +4806,7 @@ const PRAXIS_CHARTS_EN_US = {
|
|
|
4355
4806
|
const PRAXIS_CHARTS_PT_BR = {
|
|
4356
4807
|
'praxis.charts.runtime.editChart': 'Editar configurações do gráfico',
|
|
4357
4808
|
'praxis.charts.runtime.invalidDocumentTitle': 'Configuração canônica inválida',
|
|
4358
|
-
'praxis.charts.runtime.invalidDocumentDescription': 'O documento canônico do gráfico não pode ser mapeado
|
|
4809
|
+
'praxis.charts.runtime.invalidDocumentDescription': 'O documento canônico do gráfico não pode ser mapeado pelo Praxis Charts. Revise o contrato do gráfico antes de continuar.',
|
|
4359
4810
|
'praxis.charts.editor.section.general': 'Geral',
|
|
4360
4811
|
'praxis.charts.editor.section.data': 'Dados',
|
|
4361
4812
|
'praxis.charts.editor.section.analytics': 'Estrutura',
|
|
@@ -4367,7 +4818,11 @@ const PRAXIS_CHARTS_PT_BR = {
|
|
|
4367
4818
|
'praxis.charts.editor.field.kind': 'Tipo',
|
|
4368
4819
|
'praxis.charts.editor.field.title': 'Título',
|
|
4369
4820
|
'praxis.charts.editor.field.subtitle': 'Subtítulo',
|
|
4821
|
+
'praxis.charts.editor.field.sizingMode': 'Tamanho',
|
|
4370
4822
|
'praxis.charts.editor.field.height': 'Altura',
|
|
4823
|
+
'praxis.charts.editor.field.minHeight': 'Altura mínima',
|
|
4824
|
+
'praxis.charts.editor.field.maxHeight': 'Altura máxima',
|
|
4825
|
+
'praxis.charts.editor.field.aspectRatio': 'Proporção',
|
|
4371
4826
|
'praxis.charts.editor.field.sourceKind': 'Fonte',
|
|
4372
4827
|
'praxis.charts.editor.field.resource': 'Recurso',
|
|
4373
4828
|
'praxis.charts.editor.field.operation': 'Operação',
|
|
@@ -4386,7 +4841,11 @@ const PRAXIS_CHARTS_PT_BR = {
|
|
|
4386
4841
|
'praxis.charts.editor.field.legendEnabled': 'Exibir legenda',
|
|
4387
4842
|
'praxis.charts.editor.field.labelsEnabled': 'Exibir rótulos',
|
|
4388
4843
|
'praxis.charts.editor.field.tooltipEnabled': 'Exibir dica de ferramenta (tooltip)',
|
|
4844
|
+
'praxis.charts.editor.field.themeVariant': 'Variante de tema',
|
|
4845
|
+
'praxis.charts.editor.field.paletteMode': 'Modo da paleta',
|
|
4846
|
+
'praxis.charts.editor.field.paletteToken': 'Token da paleta',
|
|
4389
4847
|
'praxis.charts.editor.field.palette': 'Cores da paleta',
|
|
4848
|
+
'praxis.charts.editor.field.surfaceMode': 'Modo da superfície',
|
|
4390
4849
|
'praxis.charts.editor.field.emptyTitle': 'Título do estado vazio',
|
|
4391
4850
|
'praxis.charts.editor.field.emptyDescription': 'Descrição do estado vazio',
|
|
4392
4851
|
'praxis.charts.editor.field.loadingTitle': 'Título de carregamento',
|
|
@@ -4405,8 +4864,15 @@ const PRAXIS_CHARTS_PT_BR = {
|
|
|
4405
4864
|
'praxis.charts.editor.issues.empty': 'Nenhum problema identificado.',
|
|
4406
4865
|
'praxis.charts.editor.appearance.featuresTitle': 'Recursos visuais',
|
|
4407
4866
|
'praxis.charts.editor.appearance.paletteTitle': 'Paleta',
|
|
4408
|
-
'praxis.charts.editor.appearance.paletteHint': 'Use cores separadas por vírgula
|
|
4867
|
+
'praxis.charts.editor.appearance.paletteHint': 'Use um token registrado ou cores separadas por vírgula para persistir `theme.palette` no contrato canônico.',
|
|
4868
|
+
'praxis.charts.editor.appearance.surfaceTitle': 'Superfície',
|
|
4869
|
+
'praxis.charts.editor.appearance.surfaceHint': 'Use embutido para widgets de dashboard e contido apenas quando o gráfico precisar ser dono da própria superfície visual.',
|
|
4409
4870
|
'praxis.charts.editor.appearance.statesTitle': 'Mensagens de estado',
|
|
4871
|
+
'praxis.charts.editor.hint.sizingMode': 'Use preencher container apenas quando o widget host fornecer altura definida para o corpo.',
|
|
4872
|
+
'praxis.charts.editor.hint.height': 'Números são salvos como pixels; medidas CSS como 20rem também são aceitas.',
|
|
4873
|
+
'praxis.charts.editor.hint.minHeight': 'Defina um mínimo legível para widgets compactos de dashboard.',
|
|
4874
|
+
'praxis.charts.editor.hint.maxHeight': 'Deixe vazio exceto quando o gráfico não puder crescer dentro de um layout flexível.',
|
|
4875
|
+
'praxis.charts.editor.hint.aspectRatio': 'Opcional. Use valores como 1.777 ou 16 / 9.',
|
|
4410
4876
|
'praxis.charts.editor.analytics.dimensionsTitle': 'Dimensões',
|
|
4411
4877
|
'praxis.charts.editor.analytics.metricsTitle': 'Métricas',
|
|
4412
4878
|
'praxis.charts.editor.analytics.addDimension': 'Adicionar dimensão',
|
|
@@ -4418,6 +4884,16 @@ const PRAXIS_CHARTS_PT_BR = {
|
|
|
4418
4884
|
'praxis.charts.editor.specialization.comboTitle': 'Guia de combo',
|
|
4419
4885
|
'praxis.charts.editor.specialization.comboHint': 'Gráficos combo exigem pelo menos duas métricas e permitem configurar eixo e tipo de série por métrica.',
|
|
4420
4886
|
'praxis.charts.editor.specialization.pieDonutTitle': 'Guia de composição',
|
|
4887
|
+
'praxis.charts.editor.themeVariant.none': 'Sem variante',
|
|
4888
|
+
'praxis.charts.editor.themeVariant.default': 'Padrão',
|
|
4889
|
+
'praxis.charts.editor.themeVariant.compact': 'Compacta',
|
|
4890
|
+
'praxis.charts.editor.themeVariant.executive': 'Executiva',
|
|
4891
|
+
'praxis.charts.editor.paletteMode.token': 'Token registrado',
|
|
4892
|
+
'praxis.charts.editor.paletteMode.custom': 'Cores customizadas',
|
|
4893
|
+
'praxis.charts.editor.paletteToken.brand-primary': 'Marca principal',
|
|
4894
|
+
'praxis.charts.editor.paletteToken.brand-balanced': 'Marca balanceada',
|
|
4895
|
+
'praxis.charts.editor.paletteToken.status': 'Status',
|
|
4896
|
+
'praxis.charts.editor.paletteToken.executive': 'Executiva',
|
|
4421
4897
|
'praxis.charts.editor.specialization.pieDonutHint': 'Gráficos de pizza (pie) e rosca (donut) mantêm apenas a primeira métrica e usam a primeira dimensão como segmento categórico.',
|
|
4422
4898
|
'praxis.charts.editor.specialization.scatterTitle': 'Guia de dispersão (scatter)',
|
|
4423
4899
|
'praxis.charts.editor.specialization.scatterHint': 'Gráficos de dispersão usam a primeira dimensão como eixo X e a primeira métrica como eixo Y.',
|
|
@@ -4426,6 +4902,12 @@ const PRAXIS_CHARTS_PT_BR = {
|
|
|
4426
4902
|
'praxis.charts.editor.events.none': 'Nenhuma',
|
|
4427
4903
|
'praxis.charts.editor.sourceKind.praxisStats': 'praxis.stats',
|
|
4428
4904
|
'praxis.charts.editor.sourceKind.derived': 'derivado',
|
|
4905
|
+
'praxis.charts.editor.sizing.fixed': 'Altura fixa',
|
|
4906
|
+
'praxis.charts.editor.sizing.fill-container': 'Preencher widget',
|
|
4907
|
+
'praxis.charts.editor.sizing.auto': 'Automático',
|
|
4908
|
+
'praxis.charts.editor.surface.auto': 'Automático',
|
|
4909
|
+
'praxis.charts.editor.surface.embedded': 'Embutido no widget',
|
|
4910
|
+
'praxis.charts.editor.surface.contained': 'Superfície contida',
|
|
4429
4911
|
'praxis.charts.editor.operation.groupBy': 'agrupar por',
|
|
4430
4912
|
'praxis.charts.editor.operation.timeseries': 'série temporal',
|
|
4431
4913
|
'praxis.charts.editor.operation.distribution': 'distribuição',
|
|
@@ -4646,6 +5128,11 @@ class PraxisChartConfigEditor {
|
|
|
4646
5128
|
metricSeriesKinds = ['bar', 'line', 'area'];
|
|
4647
5129
|
motionPresets = ['subtle', 'standard', 'expressive'];
|
|
4648
5130
|
eventActionOptions = ['filter-widget', 'open-detail', 'navigate', 'update-context', 'emit'];
|
|
5131
|
+
sizingModes = ['fixed', 'fill-container', 'auto'];
|
|
5132
|
+
surfaceModes = ['auto', 'embedded', 'contained'];
|
|
5133
|
+
themeVariants = ['default', 'compact', 'executive'];
|
|
5134
|
+
paletteModes = ['token', 'custom'];
|
|
5135
|
+
paletteTokens = Object.keys(PRAXIS_CHART_PALETTE_TOKENS);
|
|
4649
5136
|
activeSection = signal('general', ...(ngDevMode ? [{ debugName: "activeSection" }] : []));
|
|
4650
5137
|
injectedData = inject(SETTINGS_PANEL_DATA, { optional: true });
|
|
4651
5138
|
defaults = inject(ChartEditorDefaultsService);
|
|
@@ -4746,9 +5233,62 @@ class PraxisChartConfigEditor {
|
|
|
4746
5233
|
}));
|
|
4747
5234
|
}
|
|
4748
5235
|
setHeight(value) {
|
|
5236
|
+
this.setSizingHeight(value);
|
|
5237
|
+
}
|
|
5238
|
+
setSizingMode(mode) {
|
|
4749
5239
|
this.patchDocument((document) => ({
|
|
4750
5240
|
...document,
|
|
4751
|
-
height:
|
|
5241
|
+
height: undefined,
|
|
5242
|
+
sizing: {
|
|
5243
|
+
...(document.sizing ?? {}),
|
|
5244
|
+
mode,
|
|
5245
|
+
height: mode === 'fixed' ? document.sizing?.height : undefined,
|
|
5246
|
+
},
|
|
5247
|
+
}));
|
|
5248
|
+
}
|
|
5249
|
+
setSizingHeight(value) {
|
|
5250
|
+
const height = this.normalizeSizingSizeInput(value);
|
|
5251
|
+
this.patchDocument((document) => ({
|
|
5252
|
+
...document,
|
|
5253
|
+
height: undefined,
|
|
5254
|
+
sizing: {
|
|
5255
|
+
...(document.sizing ?? {}),
|
|
5256
|
+
mode: 'fixed',
|
|
5257
|
+
height,
|
|
5258
|
+
},
|
|
5259
|
+
}));
|
|
5260
|
+
}
|
|
5261
|
+
setSizingMinHeight(value) {
|
|
5262
|
+
const minHeight = this.normalizeSizingSizeInput(value);
|
|
5263
|
+
this.patchDocument((document) => ({
|
|
5264
|
+
...document,
|
|
5265
|
+
height: undefined,
|
|
5266
|
+
sizing: {
|
|
5267
|
+
...(document.sizing ?? {}),
|
|
5268
|
+
minHeight,
|
|
5269
|
+
},
|
|
5270
|
+
}));
|
|
5271
|
+
}
|
|
5272
|
+
setSizingMaxHeight(value) {
|
|
5273
|
+
const maxHeight = this.normalizeSizingSizeInput(value);
|
|
5274
|
+
this.patchDocument((document) => ({
|
|
5275
|
+
...document,
|
|
5276
|
+
height: undefined,
|
|
5277
|
+
sizing: {
|
|
5278
|
+
...(document.sizing ?? {}),
|
|
5279
|
+
maxHeight,
|
|
5280
|
+
},
|
|
5281
|
+
}));
|
|
5282
|
+
}
|
|
5283
|
+
setSizingAspectRatio(value) {
|
|
5284
|
+
const aspectRatio = this.normalizeSizingSizeInput(value);
|
|
5285
|
+
this.patchDocument((document) => ({
|
|
5286
|
+
...document,
|
|
5287
|
+
height: undefined,
|
|
5288
|
+
sizing: {
|
|
5289
|
+
...(document.sizing ?? {}),
|
|
5290
|
+
aspectRatio,
|
|
5291
|
+
},
|
|
4752
5292
|
}));
|
|
4753
5293
|
}
|
|
4754
5294
|
setSourceKind(value) {
|
|
@@ -4971,6 +5511,30 @@ class PraxisChartConfigEditor {
|
|
|
4971
5511
|
},
|
|
4972
5512
|
}));
|
|
4973
5513
|
}
|
|
5514
|
+
setThemeVariant(variant) {
|
|
5515
|
+
this.patchDocument((document) => ({
|
|
5516
|
+
...document,
|
|
5517
|
+
theme: this.mergeTheme(document.theme, {
|
|
5518
|
+
variant: variant || undefined,
|
|
5519
|
+
}),
|
|
5520
|
+
}));
|
|
5521
|
+
}
|
|
5522
|
+
setPaletteMode(mode) {
|
|
5523
|
+
this.patchDocument((document) => ({
|
|
5524
|
+
...document,
|
|
5525
|
+
theme: this.mergeTheme(document.theme, {
|
|
5526
|
+
palette: mode === 'token' ? 'brand-primary' : undefined,
|
|
5527
|
+
}),
|
|
5528
|
+
}));
|
|
5529
|
+
}
|
|
5530
|
+
setPaletteToken(token) {
|
|
5531
|
+
this.patchDocument((document) => ({
|
|
5532
|
+
...document,
|
|
5533
|
+
theme: this.mergeTheme(document.theme, {
|
|
5534
|
+
palette: token,
|
|
5535
|
+
}),
|
|
5536
|
+
}));
|
|
5537
|
+
}
|
|
4974
5538
|
setPalette(value) {
|
|
4975
5539
|
const palette = value
|
|
4976
5540
|
.split(/[\n,]/)
|
|
@@ -4978,10 +5542,20 @@ class PraxisChartConfigEditor {
|
|
|
4978
5542
|
.filter(Boolean);
|
|
4979
5543
|
this.patchDocument((document) => ({
|
|
4980
5544
|
...document,
|
|
4981
|
-
theme: {
|
|
4982
|
-
...(document.theme ?? {}),
|
|
5545
|
+
theme: this.mergeTheme(document.theme, {
|
|
4983
5546
|
palette: palette.length ? palette : undefined,
|
|
4984
|
-
},
|
|
5547
|
+
}),
|
|
5548
|
+
}));
|
|
5549
|
+
}
|
|
5550
|
+
setSurfaceMode(mode) {
|
|
5551
|
+
this.patchDocument((document) => ({
|
|
5552
|
+
...document,
|
|
5553
|
+
theme: this.mergeTheme(document.theme, {
|
|
5554
|
+
surface: {
|
|
5555
|
+
...(document.theme?.surface ?? {}),
|
|
5556
|
+
mode,
|
|
5557
|
+
},
|
|
5558
|
+
}),
|
|
4985
5559
|
}));
|
|
4986
5560
|
}
|
|
4987
5561
|
setStateTitle(stateKey, value) {
|
|
@@ -5086,9 +5660,25 @@ class PraxisChartConfigEditor {
|
|
|
5086
5660
|
return value === undefined ? '' : String(value);
|
|
5087
5661
|
}
|
|
5088
5662
|
heightValue() {
|
|
5089
|
-
const
|
|
5663
|
+
const document = this.normalizedDocument();
|
|
5664
|
+
const height = document.sizing?.height;
|
|
5090
5665
|
return height === undefined || height === null ? '' : String(height);
|
|
5091
5666
|
}
|
|
5667
|
+
sizingModeValue() {
|
|
5668
|
+
return this.normalizedDocument().sizing?.mode ?? 'auto';
|
|
5669
|
+
}
|
|
5670
|
+
sizingMinHeightValue() {
|
|
5671
|
+
const minHeight = this.normalizedDocument().sizing?.minHeight;
|
|
5672
|
+
return minHeight === undefined || minHeight === null ? '' : String(minHeight);
|
|
5673
|
+
}
|
|
5674
|
+
sizingMaxHeightValue() {
|
|
5675
|
+
const maxHeight = this.normalizedDocument().sizing?.maxHeight;
|
|
5676
|
+
return maxHeight === undefined || maxHeight === null ? '' : String(maxHeight);
|
|
5677
|
+
}
|
|
5678
|
+
sizingAspectRatioValue() {
|
|
5679
|
+
const aspectRatio = this.normalizedDocument().sizing?.aspectRatio;
|
|
5680
|
+
return aspectRatio === undefined || aspectRatio === null ? '' : String(aspectRatio);
|
|
5681
|
+
}
|
|
5092
5682
|
featureEnabled(feature) {
|
|
5093
5683
|
const value = this.doc()[feature];
|
|
5094
5684
|
if (typeof value === 'boolean') {
|
|
@@ -5100,6 +5690,21 @@ class PraxisChartConfigEditor {
|
|
|
5100
5690
|
const palette = this.doc().theme?.palette;
|
|
5101
5691
|
return Array.isArray(palette) ? palette.join(', ') : '';
|
|
5102
5692
|
}
|
|
5693
|
+
paletteModeValue() {
|
|
5694
|
+
return typeof this.doc().theme?.palette === 'string' ? 'token' : 'custom';
|
|
5695
|
+
}
|
|
5696
|
+
paletteTokenValue() {
|
|
5697
|
+
const palette = this.doc().theme?.palette;
|
|
5698
|
+
return typeof palette === 'string' && this.paletteTokens.includes(palette)
|
|
5699
|
+
? palette
|
|
5700
|
+
: 'brand-primary';
|
|
5701
|
+
}
|
|
5702
|
+
themeVariantValue() {
|
|
5703
|
+
return this.normalizedDocument().theme?.variant ?? '';
|
|
5704
|
+
}
|
|
5705
|
+
surfaceModeValue() {
|
|
5706
|
+
return this.normalizedDocument().theme?.surface?.mode ?? 'auto';
|
|
5707
|
+
}
|
|
5103
5708
|
stateTitle(stateKey) {
|
|
5104
5709
|
const value = this.doc().state?.[stateKey]?.title;
|
|
5105
5710
|
if (typeof value === 'string') {
|
|
@@ -5223,6 +5828,22 @@ class PraxisChartConfigEditor {
|
|
|
5223
5828
|
return accumulator;
|
|
5224
5829
|
}, {});
|
|
5225
5830
|
}
|
|
5831
|
+
normalizeSizingSizeInput(value) {
|
|
5832
|
+
const trimmed = value.trim();
|
|
5833
|
+
if (!trimmed) {
|
|
5834
|
+
return undefined;
|
|
5835
|
+
}
|
|
5836
|
+
return /^\d+(\.\d+)?$/.test(trimmed) ? Number(trimmed) : trimmed;
|
|
5837
|
+
}
|
|
5838
|
+
mergeTheme(currentTheme, patch) {
|
|
5839
|
+
const nextTheme = {
|
|
5840
|
+
...(currentTheme ?? {}),
|
|
5841
|
+
...(patch ?? {}),
|
|
5842
|
+
};
|
|
5843
|
+
return Object.values(nextTheme).some((value) => value !== undefined)
|
|
5844
|
+
? nextTheme
|
|
5845
|
+
: undefined;
|
|
5846
|
+
}
|
|
5226
5847
|
createApplyPayload() {
|
|
5227
5848
|
return {
|
|
5228
5849
|
document: this.getSettingsValue(),
|
|
@@ -5250,7 +5871,7 @@ class PraxisChartConfigEditor {
|
|
|
5250
5871
|
this.documentChange.emit(structuredClone(this.normalizedDocument()));
|
|
5251
5872
|
}
|
|
5252
5873
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: PraxisChartConfigEditor, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
5253
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.17", type: PraxisChartConfigEditor, isStandalone: true, selector: "praxis-chart-config-editor", inputs: { documentInput: { classPropertyName: "documentInput", publicName: "document", isSignal: true, isRequired: false, transformFunction: null }, modeInput: { classPropertyName: "modeInput", publicName: "mode", isSignal: true, isRequired: false, transformFunction: null }, readonlyInput: { classPropertyName: "readonlyInput", publicName: "readonly", isSignal: true, isRequired: false, transformFunction: null }, availableResourcesInput: { classPropertyName: "availableResourcesInput", publicName: "availableResources", isSignal: true, isRequired: false, transformFunction: null }, availableFieldsInput: { classPropertyName: "availableFieldsInput", publicName: "availableFields", isSignal: true, isRequired: false, transformFunction: null }, availableTargetsInput: { classPropertyName: "availableTargetsInput", publicName: "availableTargets", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { apply: "apply", save: "save", resetChange: "resetChange", documentChange: "documentChange" }, providers: [providePraxisChartsI18n()], ngImport: i0, template: "<div class=\"editor-shell\">\n <div class=\"editor-nav\">\n @for (section of sections; track section.id) {\n <button\n mat-stroked-button\n type=\"button\"\n [class.active]=\"activeSection() === section.id\"\n (click)=\"setSection(section.id)\"\n >\n {{ t(section.labelKey, section.fallback) }}\n </button>\n }\n </div>\n\n <div class=\"editor-layout\">\n <div class=\"editor-form\">\n <mat-card class=\"editor-card\">\n <mat-card-content>\n @switch (activeSection()) {\n @case ('general') {\n <div class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.chartId', 'Chart ID') }}</mat-label>\n <input matInput [ngModel]=\"doc().chartId || ''\" (ngModelChange)=\"setChartId($event)\" [disabled]=\"isReadonly()\" />\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.kind', 'Kind') }}</mat-label>\n <mat-select [ngModel]=\"doc().kind\" (ngModelChange)=\"setKind($event)\" [disabled]=\"isReadonly()\">\n @for (kind of chartKinds; track kind) {\n <mat-option [value]=\"kind\">\n {{ t('praxis.charts.editor.kind.' + kind, kind) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.title', 'Title') }}</mat-label>\n <input matInput [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.height', 'Height') }}</mat-label>\n <input matInput [ngModel]=\"heightValue()\" (ngModelChange)=\"setHeight($event)\" [disabled]=\"isReadonly()\" />\n </mat-form-field>\n </div>\n }\n\n @case ('data') {\n <div class=\"editor-stack\">\n <div class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.sourceKind', 'Source') }}</mat-label>\n <mat-select [ngModel]=\"doc().source.kind\" (ngModelChange)=\"setSourceKind($event)\" [disabled]=\"isReadonly()\">\n @for (sourceKind of sourceKinds; track sourceKind) {\n <mat-option [value]=\"sourceKind\">\n {{ t('praxis.charts.editor.sourceKind.' + (sourceKind === 'praxis.stats' ? 'praxisStats' : 'derived'), sourceKind) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n @if (doc().source.kind === 'praxis.stats') {\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.resource', 'Resource') }}</mat-label>\n @if (resourceOptions().length) {\n <mat-select [ngModel]=\"resourceValue()\" (ngModelChange)=\"setResource($event)\" [disabled]=\"isReadonly()\">\n @for (resource of resourceOptions(); track resource.id) {\n <mat-option [value]=\"resource.path\">{{ resource.label }}</mat-option>\n }\n </mat-select>\n } @else {\n <input matInput [ngModel]=\"resourceValue()\" (ngModelChange)=\"setResource($event)\" [disabled]=\"isReadonly()\" />\n }\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.operation', 'Operation') }}</mat-label>\n <mat-select\n [ngModel]=\"doc().source.operation || 'group-by'\"\n (ngModelChange)=\"setOperation($event)\"\n [disabled]=\"isReadonly()\"\n >\n @for (operation of operations; track operation) {\n <mat-option [value]=\"operation\">\n {{ t('praxis.charts.editor.operation.' + (operation === 'group-by' ? 'groupBy' : operation), operation) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n }\n </div>\n\n @if (showTimeseriesControls()) {\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.specialization.timeseriesTitle', 'Timeseries options') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.granularity', 'Granularity') }}</mat-label>\n <mat-select [ngModel]=\"granularityValue()\" (ngModelChange)=\"setGranularity($event)\" [disabled]=\"isReadonly()\">\n @for (granularity of timeGranularities; track granularity) {\n <mat-option [value]=\"granularity\">\n {{ t('praxis.charts.editor.granularity.' + granularity, granularity) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n <mat-slide-toggle\n [ngModel]=\"fillGapsValue()\"\n (ngModelChange)=\"setFillGaps($event)\"\n [disabled]=\"isReadonly()\"\n >\n {{ t('praxis.charts.editor.field.fillGaps', 'Fill missing intervals') }}\n </mat-slide-toggle>\n </mat-card-content>\n </mat-card>\n }\n\n @if (showDistributionControls()) {\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.specialization.distributionTitle', 'Distribution options') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.distributionMode', 'Distribution mode') }}</mat-label>\n <mat-select [ngModel]=\"distributionModeValue()\" (ngModelChange)=\"setDistributionMode($event)\" [disabled]=\"isReadonly()\">\n @for (mode of distributionModes; track mode) {\n <mat-option [value]=\"mode\">\n {{ t('praxis.charts.editor.distributionMode.' + mode, mode) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.bucketSize', 'Bucket size') }}</mat-label>\n <input matInput [ngModel]=\"bucketSizeValue()\" (ngModelChange)=\"setBucketSize($event)\" [disabled]=\"isReadonly()\" />\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.bucketCount', 'Bucket count') }}</mat-label>\n <input matInput [ngModel]=\"bucketCountValue()\" (ngModelChange)=\"setBucketCount($event)\" [disabled]=\"isReadonly()\" />\n </mat-form-field>\n </mat-card-content>\n </mat-card>\n }\n </div>\n }\n\n @case ('motion') {\n <div class=\"editor-grid\">\n <mat-slide-toggle\n [ngModel]=\"normalizedDocument().motion?.enabled !== false\"\n (ngModelChange)=\"setMotionEnabled($event)\"\n [disabled]=\"isReadonly()\"\n >\n {{ t('praxis.charts.editor.field.motionEnabled', 'Enable animations') }}\n </mat-slide-toggle>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.motionPreset', 'Motion preset') }}</mat-label>\n <mat-select\n [ngModel]=\"normalizedDocument().motion?.preset || 'standard'\"\n (ngModelChange)=\"setMotionPreset($event)\"\n [disabled]=\"isReadonly() || normalizedDocument().motion?.enabled === false\"\n >\n @for (preset of motionPresets; track preset) {\n <mat-option [value]=\"preset\">\n {{ t('praxis.charts.editor.motionPreset.' + preset, preset) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n </div>\n }\n\n @case ('appearance') {\n <div class=\"editor-stack\">\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.appearance.featuresTitle', 'Display features') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-grid\">\n <mat-slide-toggle\n [ngModel]=\"featureEnabled('legend')\"\n (ngModelChange)=\"setFeatureEnabled('legend', $event)\"\n [disabled]=\"isReadonly()\"\n >\n {{ t('praxis.charts.editor.field.legendEnabled', 'Show legend') }}\n </mat-slide-toggle>\n\n <mat-slide-toggle\n [ngModel]=\"featureEnabled('labels')\"\n (ngModelChange)=\"setFeatureEnabled('labels', $event)\"\n [disabled]=\"isReadonly()\"\n >\n {{ t('praxis.charts.editor.field.labelsEnabled', 'Show labels') }}\n </mat-slide-toggle>\n\n <mat-slide-toggle\n [ngModel]=\"featureEnabled('tooltip')\"\n (ngModelChange)=\"setFeatureEnabled('tooltip', $event)\"\n [disabled]=\"isReadonly()\"\n >\n {{ t('praxis.charts.editor.field.tooltipEnabled', 'Show tooltip') }}\n </mat-slide-toggle>\n </mat-card-content>\n </mat-card>\n\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.appearance.paletteTitle', 'Palette') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content>\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.palette', 'Palette colors') }}</mat-label>\n <textarea\n matInput\n rows=\"3\"\n [ngModel]=\"paletteValue()\"\n (ngModelChange)=\"setPalette($event)\"\n [disabled]=\"isReadonly()\"\n ></textarea>\n </mat-form-field>\n <p class=\"editor-caption\">\n {{ t('praxis.charts.editor.appearance.paletteHint', 'Use comma or line separated colors to persist theme.palette as a canonical array.') }}\n </p>\n </mat-card-content>\n </mat-card>\n\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.appearance.statesTitle', 'State messages') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-stack\">\n <div class=\"editor-row-card\">\n <div class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.emptyTitle', 'Empty title') }}</mat-label>\n <input matInput [ngModel]=\"stateTitle('empty')\" (ngModelChange)=\"setStateTitle('empty', $event)\" [disabled]=\"isReadonly()\" />\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.emptyDescription', 'Empty description') }}</mat-label>\n <textarea matInput rows=\"2\" [ngModel]=\"stateDescription('empty')\" (ngModelChange)=\"setStateDescription('empty', $event)\" [disabled]=\"isReadonly()\"></textarea>\n </mat-form-field>\n </div>\n </div>\n\n <div class=\"editor-row-card\">\n <div class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.loadingTitle', 'Loading title') }}</mat-label>\n <input matInput [ngModel]=\"stateTitle('loading')\" (ngModelChange)=\"setStateTitle('loading', $event)\" [disabled]=\"isReadonly()\" />\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.loadingDescription', 'Loading description') }}</mat-label>\n <textarea matInput rows=\"2\" [ngModel]=\"stateDescription('loading')\" (ngModelChange)=\"setStateDescription('loading', $event)\" [disabled]=\"isReadonly()\"></textarea>\n </mat-form-field>\n </div>\n </div>\n\n <div class=\"editor-row-card\">\n <div class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.errorTitle', 'Error title') }}</mat-label>\n <input matInput [ngModel]=\"stateTitle('error')\" (ngModelChange)=\"setStateTitle('error', $event)\" [disabled]=\"isReadonly()\" />\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.errorDescription', 'Error description') }}</mat-label>\n <textarea matInput rows=\"2\" [ngModel]=\"stateDescription('error')\" (ngModelChange)=\"setStateDescription('error', $event)\" [disabled]=\"isReadonly()\"></textarea>\n </mat-form-field>\n </div>\n </div>\n </mat-card-content>\n </mat-card>\n </div>\n }\n\n @case ('analytics') {\n <div class=\"editor-stack\">\n @if (showComboPanel()) {\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.specialization.comboTitle', 'Combo guidance') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content>\n <p class=\"editor-caption\">\n {{ t('praxis.charts.editor.specialization.comboHint', 'Combo charts require at least two metrics and allow per-metric axis and series kind mapping.') }}\n </p>\n </mat-card-content>\n </mat-card>\n }\n\n @if (showPieDonutPanel()) {\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.specialization.pieDonutTitle', 'Composition guidance') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content>\n <p class=\"editor-caption\">\n {{ t('praxis.charts.editor.specialization.pieDonutHint', 'Pie and donut charts keep only the first metric and use the first dimension as the category segment.') }}\n </p>\n </mat-card-content>\n </mat-card>\n }\n\n @if (showScatterPanel()) {\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.specialization.scatterTitle', 'Scatter guidance') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content>\n <p class=\"editor-caption\">\n {{ t('praxis.charts.editor.specialization.scatterHint', 'Scatter charts use the first dimension as X and the first metric as Y, so keep both fields mapped.') }}\n </p>\n </mat-card-content>\n </mat-card>\n }\n\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.analytics.dimensionsTitle', 'Dimensions') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-stack\">\n @for (dimension of dimensions(); track $index) {\n <div class=\"editor-row-card\">\n <div class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.dimension', 'Dimension') }}</mat-label>\n @if (fieldOptions('dimension').length) {\n <mat-select [ngModel]=\"dimension.field || ''\" (ngModelChange)=\"setDimensionField($index, $event)\" [disabled]=\"isReadonly()\">\n @for (field of fieldOptions('dimension'); track field.field) {\n <mat-option [value]=\"field.field\">{{ field.label || field.field }}</mat-option>\n }\n </mat-select>\n } @else {\n <input matInput [ngModel]=\"dimension.field || ''\" (ngModelChange)=\"setDimensionField($index, $event)\" [disabled]=\"isReadonly()\" />\n }\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\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\" (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\">\n <div class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.metric', 'Metric') }}</mat-label>\n @if (fieldOptions('metric').length) {\n <mat-select [ngModel]=\"metric.field || ''\" (ngModelChange)=\"setMetricField($index, $event)\" [disabled]=\"isReadonly()\">\n @for (field of fieldOptions('metric'); track field.field) {\n <mat-option [value]=\"field.field\">{{ field.label || field.field }}</mat-option>\n }\n </mat-select>\n } @else {\n <input matInput [ngModel]=\"metric.field || ''\" (ngModelChange)=\"setMetricField($index, $event)\" [disabled]=\"isReadonly()\" />\n }\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\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\">\n <mat-label>{{ t('praxis.charts.editor.field.metricAggregation', 'Aggregation') }}</mat-label>\n <mat-select [ngModel]=\"metric.aggregation || 'sum'\" (ngModelChange)=\"setMetricAggregation($index, $event)\" [disabled]=\"isReadonly()\">\n @for (aggregation of metricAggregations; track aggregation) {\n <mat-option [value]=\"aggregation\">\n {{ t('praxis.charts.editor.metricAggregation.' + aggregation, aggregation) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n @if (showMetricAxisControls()) {\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\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\">\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\" (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\" (click)=\"addMetric()\" [disabled]=\"isReadonly()\">\n {{ t('praxis.charts.editor.analytics.addMetric', 'Add metric') }}\n </button>\n </div>\n </mat-card-content>\n </mat-card>\n </div>\n }\n\n @case ('events') {\n <div class=\"editor-stack\">\n <mat-card class=\"editor-card\">\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 [ngModel]=\"eventAction('pointClick')\" (ngModelChange)=\"setEventAction('pointClick', $event)\" [disabled]=\"isReadonly()\">\n <mat-option value=\"\">{{ t('praxis.charts.editor.events.none', 'None') }}</mat-option>\n @for (action of eventActionOptions; track action) {\n <mat-option [value]=\"action\">\n {{ t('praxis.charts.editor.eventAction.' + action, action) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventTarget', 'Target') }}</mat-label>\n @if (targetOptions().length) {\n <mat-select [ngModel]=\"eventTarget('pointClick')\" (ngModelChange)=\"setEventTarget('pointClick', $event)\" [disabled]=\"isReadonly() || !eventAction('pointClick')\">\n @for (target of targetOptions(); track target.id) {\n <mat-option [value]=\"target.id\">{{ target.label }}</mat-option>\n }\n </mat-select>\n } @else {\n <input matInput [ngModel]=\"eventTarget('pointClick')\" (ngModelChange)=\"setEventTarget('pointClick', $event)\" [disabled]=\"isReadonly() || !eventAction('pointClick')\" />\n }\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventMapping', 'Mapping') }}</mat-label>\n <textarea\n matInput\n 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\">\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 [ngModel]=\"eventAction('drillDown')\" (ngModelChange)=\"setEventAction('drillDown', $event)\" [disabled]=\"isReadonly()\">\n <mat-option value=\"\">{{ t('praxis.charts.editor.events.none', 'None') }}</mat-option>\n @for (action of eventActionOptions; track action) {\n <mat-option [value]=\"action\">\n {{ t('praxis.charts.editor.eventAction.' + action, action) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventTarget', 'Target') }}</mat-label>\n @if (targetOptions().length) {\n <mat-select [ngModel]=\"eventTarget('drillDown')\" (ngModelChange)=\"setEventTarget('drillDown', $event)\" [disabled]=\"isReadonly() || !eventAction('drillDown')\">\n @for (target of targetOptions(); track target.id) {\n <mat-option [value]=\"target.id\">{{ target.label }}</mat-option>\n }\n </mat-select>\n } @else {\n <input matInput [ngModel]=\"eventTarget('drillDown')\" (ngModelChange)=\"setEventTarget('drillDown', $event)\" [disabled]=\"isReadonly() || !eventAction('drillDown')\" />\n }\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventMapping', 'Mapping') }}</mat-label>\n <textarea\n matInput\n 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 </div>\n }\n\n @case ('preview') {\n <p class=\"editor-caption\">\n {{ t('praxis.charts.editor.preview.caption', 'Local preview derived from the canonical contract without remote calls.') }}\n </p>\n }\n }\n </mat-card-content>\n </mat-card>\n </div>\n\n <div class=\"editor-side\">\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.issues.title', 'Validation issues') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content>\n @if (issues().length) {\n <ul class=\"editor-issues\">\n @for (issue of issues(); track issueTrackBy($index, issue)) {\n <li class=\"editor-issue\">\n <strong>{{ issue.field }}</strong>\n <span>{{ issue.message }}</span>\n </li>\n }\n </ul>\n } @else {\n <div class=\"editor-empty\">\n {{ t('praxis.charts.editor.issues.empty', 'No issues were identified.') }}\n </div>\n }\n </mat-card-content>\n </mat-card>\n\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.preview.title', 'Chart preview') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content>\n @if (preview(); as chartPreview) {\n <p class=\"editor-caption\">\n {{ t('praxis.charts.editor.preview.caption', 'Local preview derived from the canonical contract without remote calls.') }}\n </p>\n <praxis-chart [config]=\"chartPreview.config\" [data]=\"chartPreview.data\"></praxis-chart>\n } @else {\n <div class=\"editor-empty\">\n {{ t('praxis.charts.editor.preview.invalid', 'Preview is unavailable while the contract has blocking errors.') }}\n </div>\n }\n </mat-card-content>\n </mat-card>\n </div>\n </div>\n</div>\n", styles: [":host{display:block;min-width:0;color:var(--md-sys-color-on-surface, #1a1b20)}.editor-shell{display:grid;gap:18px}.editor-nav{display:flex;gap:8px;flex-wrap:wrap}.editor-nav button.active{background:color-mix(in srgb,var(--md-sys-color-primary, #1263b4) 18%,transparent);color:var(--md-sys-color-primary, #1263b4)}.editor-layout{display:grid;gap:18px;grid-template-columns:minmax(0,1.35fr) minmax(320px,.9fr);align-items:start}.editor-form,.editor-side{display:grid;gap:16px}.editor-card{border-radius:20px;border:1px solid color-mix(in srgb,var(--md-sys-color-outline, #c5c7ce) 72%,transparent);background:linear-gradient(180deg,#1263b408,#1263b400)}.editor-grid{display:grid;gap:14px;grid-template-columns:repeat(auto-fit,minmax(220px,1fr))}.editor-stack{display:grid;gap:14px}.editor-row-card{padding:14px;border-radius:16px;border:1px solid color-mix(in srgb,var(--md-sys-color-outline, #c5c7ce) 54%,transparent);background:color-mix(in srgb,var(--md-sys-color-surface, #fff) 92%,rgba(18,99,180,.04))}.editor-row-actions{display:flex;justify-content:flex-end}.editor-field{width:100%}.editor-issues{display:grid;gap:10px;margin:0;padding:0;list-style:none}.editor-issue{padding:12px 14px;border-radius:14px;background:color-mix(in srgb,var(--md-sys-color-error, #b3261e) 8%,transparent);border:1px solid color-mix(in srgb,var(--md-sys-color-error, #b3261e) 18%,transparent)}.editor-issue strong{display:block;margin-bottom:4px}.editor-caption{margin:0 0 12px;color:var(--md-sys-color-on-surface-variant, #5a5d67);font-size:.92rem}.editor-empty{padding:18px;border-radius:14px;background:color-mix(in srgb,var(--md-sys-color-surface-variant, #eceff4) 78%,transparent)}@media(max-width:960px){.editor-layout{grid-template-columns:minmax(0,1fr)}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$3.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$3.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$3.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i1$1.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: MatCardModule }, { kind: "component", type: i3$1.MatCard, selector: "mat-card", inputs: ["appearance"], exportAs: ["matCard"] }, { kind: "directive", type: i3$1.MatCardContent, selector: "mat-card-content" }, { kind: "component", type: i3$1.MatCardHeader, selector: "mat-card-header" }, { kind: "directive", type: i3$1.MatCardTitle, selector: "mat-card-title, [mat-card-title], [matCardTitle]" }, { kind: "ngmodule", type: MatFormFieldModule }, { kind: "component", type: i4.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i4.MatLabel, selector: "mat-label" }, { kind: "ngmodule", type: MatInputModule }, { kind: "directive", type: i5.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["disabled", "id", "placeholder", "name", "required", "type", "errorStateMatcher", "aria-describedby", "value", "readonly", "disabledInteractive"], exportAs: ["matInput"] }, { kind: "ngmodule", type: MatSelectModule }, { kind: "component", type: i6.MatSelect, selector: "mat-select", inputs: ["aria-describedby", "panelClass", "disabled", "disableRipple", "tabIndex", "hideSingleSelectionIndicator", "placeholder", "required", "multiple", "disableOptionCentering", "compareWith", "value", "aria-label", "aria-labelledby", "errorStateMatcher", "typeaheadDebounceInterval", "sortComparator", "id", "panelWidth", "canSelectNullableOptions"], outputs: ["openedChange", "opened", "closed", "selectionChange", "valueChange"], exportAs: ["matSelect"] }, { kind: "component", type: i6.MatOption, selector: "mat-option", inputs: ["value", "id", "disabled"], outputs: ["onSelectionChange"], exportAs: ["matOption"] }, { kind: "ngmodule", type: MatSlideToggleModule }, { kind: "component", type: i7.MatSlideToggle, selector: "mat-slide-toggle", inputs: ["name", "id", "labelPosition", "aria-label", "aria-labelledby", "aria-describedby", "required", "color", "disabled", "disableRipple", "tabIndex", "checked", "hideIcon", "disabledInteractive"], outputs: ["change", "toggleChange"], exportAs: ["matSlideToggle"] }, { kind: "component", type: PraxisChartComponent, selector: "praxis-chart", inputs: ["config", "data", "chartDocument", "filterCriteria", "queryContext", "enableCustomization", "availableResources", "availableFields", "availableTargets"], outputs: ["pointClick", "queryRequest", "loadStateChange", "chartDocumentApplied", "chartDocumentSaved"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
5874
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.17", type: PraxisChartConfigEditor, isStandalone: true, selector: "praxis-chart-config-editor", inputs: { documentInput: { classPropertyName: "documentInput", publicName: "document", isSignal: true, isRequired: false, transformFunction: null }, modeInput: { classPropertyName: "modeInput", publicName: "mode", isSignal: true, isRequired: false, transformFunction: null }, readonlyInput: { classPropertyName: "readonlyInput", publicName: "readonly", isSignal: true, isRequired: false, transformFunction: null }, availableResourcesInput: { classPropertyName: "availableResourcesInput", publicName: "availableResources", isSignal: true, isRequired: false, transformFunction: null }, availableFieldsInput: { classPropertyName: "availableFieldsInput", publicName: "availableFields", isSignal: true, isRequired: false, transformFunction: null }, availableTargetsInput: { classPropertyName: "availableTargetsInput", publicName: "availableTargets", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { apply: "apply", save: "save", resetChange: "resetChange", documentChange: "documentChange" }, providers: [providePraxisChartsI18n()], ngImport: i0, template: "<div class=\"editor-shell\">\n <div class=\"editor-nav\">\n @for (section of sections; track section.id) {\n <button\n mat-stroked-button\n type=\"button\"\n [class.active]=\"activeSection() === section.id\"\n (click)=\"setSection(section.id)\"\n >\n {{ t(section.labelKey, section.fallback) }}\n </button>\n }\n </div>\n\n <div class=\"editor-layout\">\n <div class=\"editor-form\">\n <mat-card class=\"editor-card\">\n <mat-card-content>\n @switch (activeSection()) {\n @case ('general') {\n <div class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.chartId', 'Chart ID') }}</mat-label>\n <input matInput [ngModel]=\"doc().chartId || ''\" (ngModelChange)=\"setChartId($event)\" [disabled]=\"isReadonly()\" />\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.kind', 'Kind') }}</mat-label>\n <mat-select [ngModel]=\"doc().kind\" (ngModelChange)=\"setKind($event)\" [disabled]=\"isReadonly()\">\n @for (kind of chartKinds; track kind) {\n <mat-option [value]=\"kind\">\n {{ t('praxis.charts.editor.kind.' + kind, kind) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.title', 'Title') }}</mat-label>\n <input matInput [ngModel]=\"titleValue()\" (ngModelChange)=\"setTitle($event)\" [disabled]=\"isReadonly()\" />\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.subtitle', 'Subtitle') }}</mat-label>\n <input matInput [ngModel]=\"subtitleValue()\" (ngModelChange)=\"setSubtitle($event)\" [disabled]=\"isReadonly()\" />\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.sizingMode', 'Sizing') }}</mat-label>\n <mat-select [ngModel]=\"sizingModeValue()\" (ngModelChange)=\"setSizingMode($event)\" [disabled]=\"isReadonly()\">\n @for (mode of sizingModes; track mode) {\n <mat-option [value]=\"mode\">\n {{ t('praxis.charts.editor.sizing.' + mode, mode) }}\n </mat-option>\n }\n </mat-select>\n <mat-hint>{{ t('praxis.charts.editor.hint.sizingMode', 'Use fill-container only when the host widget provides a defined body height.') }}</mat-hint>\n </mat-form-field>\n\n @if (sizingModeValue() === 'fixed') {\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.height', 'Height') }}</mat-label>\n <input matInput [ngModel]=\"heightValue()\" (ngModelChange)=\"setSizingHeight($event)\" [disabled]=\"isReadonly()\" />\n <mat-hint>{{ t('praxis.charts.editor.hint.height', 'Numbers are saved as pixels; CSS lengths such as 20rem are also accepted.') }}</mat-hint>\n </mat-form-field>\n }\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.minHeight', 'Minimum height') }}</mat-label>\n <input matInput [ngModel]=\"sizingMinHeightValue()\" (ngModelChange)=\"setSizingMinHeight($event)\" [disabled]=\"isReadonly()\" />\n <mat-hint>{{ t('praxis.charts.editor.hint.minHeight', 'Set a readable minimum for compact dashboard widgets.') }}</mat-hint>\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.maxHeight', 'Maximum height') }}</mat-label>\n <input matInput [ngModel]=\"sizingMaxHeightValue()\" (ngModelChange)=\"setSizingMaxHeight($event)\" [disabled]=\"isReadonly()\" />\n <mat-hint>{{ t('praxis.charts.editor.hint.maxHeight', 'Leave empty unless the chart must stop growing inside a flexible layout.') }}</mat-hint>\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.aspectRatio', 'Aspect ratio') }}</mat-label>\n <input matInput [ngModel]=\"sizingAspectRatioValue()\" (ngModelChange)=\"setSizingAspectRatio($event)\" [disabled]=\"isReadonly()\" />\n <mat-hint>{{ t('praxis.charts.editor.hint.aspectRatio', 'Optional. Use values such as 1.777 or 16 / 9.') }}</mat-hint>\n </mat-form-field>\n </div>\n }\n\n @case ('data') {\n <div class=\"editor-stack\">\n <div class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.sourceKind', 'Source') }}</mat-label>\n <mat-select [ngModel]=\"doc().source.kind\" (ngModelChange)=\"setSourceKind($event)\" [disabled]=\"isReadonly()\">\n @for (sourceKind of sourceKinds; track sourceKind) {\n <mat-option [value]=\"sourceKind\">\n {{ t('praxis.charts.editor.sourceKind.' + (sourceKind === 'praxis.stats' ? 'praxisStats' : 'derived'), sourceKind) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n @if (doc().source.kind === 'praxis.stats') {\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.resource', 'Resource') }}</mat-label>\n @if (resourceOptions().length) {\n <mat-select [ngModel]=\"resourceValue()\" (ngModelChange)=\"setResource($event)\" [disabled]=\"isReadonly()\">\n @for (resource of resourceOptions(); track resource.id) {\n <mat-option [value]=\"resource.path\">{{ resource.label }}</mat-option>\n }\n </mat-select>\n } @else {\n <input matInput [ngModel]=\"resourceValue()\" (ngModelChange)=\"setResource($event)\" [disabled]=\"isReadonly()\" />\n }\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.operation', 'Operation') }}</mat-label>\n <mat-select\n [ngModel]=\"doc().source.operation || 'group-by'\"\n (ngModelChange)=\"setOperation($event)\"\n [disabled]=\"isReadonly()\"\n >\n @for (operation of operations; track operation) {\n <mat-option [value]=\"operation\">\n {{ t('praxis.charts.editor.operation.' + (operation === 'group-by' ? 'groupBy' : operation), operation) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n }\n </div>\n\n @if (showTimeseriesControls()) {\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.specialization.timeseriesTitle', 'Timeseries options') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.granularity', 'Granularity') }}</mat-label>\n <mat-select [ngModel]=\"granularityValue()\" (ngModelChange)=\"setGranularity($event)\" [disabled]=\"isReadonly()\">\n @for (granularity of timeGranularities; track granularity) {\n <mat-option [value]=\"granularity\">\n {{ t('praxis.charts.editor.granularity.' + granularity, granularity) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n <mat-slide-toggle\n [ngModel]=\"fillGapsValue()\"\n (ngModelChange)=\"setFillGaps($event)\"\n [disabled]=\"isReadonly()\"\n >\n {{ t('praxis.charts.editor.field.fillGaps', 'Fill missing intervals') }}\n </mat-slide-toggle>\n </mat-card-content>\n </mat-card>\n }\n\n @if (showDistributionControls()) {\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.specialization.distributionTitle', 'Distribution options') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.distributionMode', 'Distribution mode') }}</mat-label>\n <mat-select [ngModel]=\"distributionModeValue()\" (ngModelChange)=\"setDistributionMode($event)\" [disabled]=\"isReadonly()\">\n @for (mode of distributionModes; track mode) {\n <mat-option [value]=\"mode\">\n {{ t('praxis.charts.editor.distributionMode.' + mode, mode) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.bucketSize', 'Bucket size') }}</mat-label>\n <input matInput [ngModel]=\"bucketSizeValue()\" (ngModelChange)=\"setBucketSize($event)\" [disabled]=\"isReadonly()\" />\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.bucketCount', 'Bucket count') }}</mat-label>\n <input matInput [ngModel]=\"bucketCountValue()\" (ngModelChange)=\"setBucketCount($event)\" [disabled]=\"isReadonly()\" />\n </mat-form-field>\n </mat-card-content>\n </mat-card>\n }\n </div>\n }\n\n @case ('motion') {\n <div class=\"editor-grid\">\n <mat-slide-toggle\n [ngModel]=\"normalizedDocument().motion?.enabled !== false\"\n (ngModelChange)=\"setMotionEnabled($event)\"\n [disabled]=\"isReadonly()\"\n >\n {{ t('praxis.charts.editor.field.motionEnabled', 'Enable animations') }}\n </mat-slide-toggle>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.motionPreset', 'Motion preset') }}</mat-label>\n <mat-select\n [ngModel]=\"normalizedDocument().motion?.preset || 'standard'\"\n (ngModelChange)=\"setMotionPreset($event)\"\n [disabled]=\"isReadonly() || normalizedDocument().motion?.enabled === false\"\n >\n @for (preset of motionPresets; track preset) {\n <mat-option [value]=\"preset\">\n {{ t('praxis.charts.editor.motionPreset.' + preset, preset) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n </div>\n }\n\n @case ('appearance') {\n <div class=\"editor-stack\">\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.appearance.featuresTitle', 'Display features') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-grid\">\n <mat-slide-toggle\n [ngModel]=\"featureEnabled('legend')\"\n (ngModelChange)=\"setFeatureEnabled('legend', $event)\"\n [disabled]=\"isReadonly()\"\n >\n {{ t('praxis.charts.editor.field.legendEnabled', 'Show legend') }}\n </mat-slide-toggle>\n\n <mat-slide-toggle\n [ngModel]=\"featureEnabled('labels')\"\n (ngModelChange)=\"setFeatureEnabled('labels', $event)\"\n [disabled]=\"isReadonly()\"\n >\n {{ t('praxis.charts.editor.field.labelsEnabled', 'Show labels') }}\n </mat-slide-toggle>\n\n <mat-slide-toggle\n [ngModel]=\"featureEnabled('tooltip')\"\n (ngModelChange)=\"setFeatureEnabled('tooltip', $event)\"\n [disabled]=\"isReadonly()\"\n >\n {{ t('praxis.charts.editor.field.tooltipEnabled', 'Show tooltip') }}\n </mat-slide-toggle>\n </mat-card-content>\n </mat-card>\n\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.appearance.paletteTitle', 'Palette') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-stack\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.themeVariant', 'Theme variant') }}</mat-label>\n <mat-select [ngModel]=\"themeVariantValue()\" (ngModelChange)=\"setThemeVariant($event)\" [disabled]=\"isReadonly()\">\n <mat-option [value]=\"''\">\n {{ t('praxis.charts.editor.themeVariant.none', 'No variant') }}\n </mat-option>\n @for (variant of themeVariants; track variant) {\n <mat-option [value]=\"variant\">\n {{ t('praxis.charts.editor.themeVariant.' + variant, variant) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.paletteMode', 'Palette mode') }}</mat-label>\n <mat-select [ngModel]=\"paletteModeValue()\" (ngModelChange)=\"setPaletteMode($event)\" [disabled]=\"isReadonly()\">\n @for (mode of paletteModes; track mode) {\n <mat-option [value]=\"mode\">\n {{ t('praxis.charts.editor.paletteMode.' + mode, mode) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n @if (paletteModeValue() === 'token') {\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.paletteToken', 'Palette token') }}</mat-label>\n <mat-select [ngModel]=\"paletteTokenValue()\" (ngModelChange)=\"setPaletteToken($event)\" [disabled]=\"isReadonly()\">\n @for (token of paletteTokens; track token) {\n <mat-option [value]=\"token\">\n {{ t('praxis.charts.editor.paletteToken.' + token, token) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n } @else {\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.palette', 'Palette colors') }}</mat-label>\n <textarea\n matInput\n rows=\"3\"\n [ngModel]=\"paletteValue()\"\n (ngModelChange)=\"setPalette($event)\"\n [disabled]=\"isReadonly()\"\n ></textarea>\n </mat-form-field>\n }\n\n <p class=\"editor-caption\">\n {{ t('praxis.charts.editor.appearance.paletteHint', 'Use a registered token or comma-separated colors to persist theme.palette in the canonical contract.') }}\n </p>\n </mat-card-content>\n </mat-card>\n\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.appearance.surfaceTitle', 'Surface') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content>\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.surfaceMode', 'Surface mode') }}</mat-label>\n <mat-select [ngModel]=\"surfaceModeValue()\" (ngModelChange)=\"setSurfaceMode($event)\" [disabled]=\"isReadonly()\">\n @for (mode of surfaceModes; track mode) {\n <mat-option [value]=\"mode\">\n {{ t('praxis.charts.editor.surface.' + mode, mode) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n <p class=\"editor-caption\">\n {{ t('praxis.charts.editor.appearance.surfaceHint', 'Use embedded for dashboard widgets and contained only when the chart must own its visual surface.') }}\n </p>\n </mat-card-content>\n </mat-card>\n\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.appearance.statesTitle', 'State messages') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-stack\">\n <div class=\"editor-row-card\">\n <div class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.emptyTitle', 'Empty title') }}</mat-label>\n <input matInput [ngModel]=\"stateTitle('empty')\" (ngModelChange)=\"setStateTitle('empty', $event)\" [disabled]=\"isReadonly()\" />\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.emptyDescription', 'Empty description') }}</mat-label>\n <textarea matInput rows=\"2\" [ngModel]=\"stateDescription('empty')\" (ngModelChange)=\"setStateDescription('empty', $event)\" [disabled]=\"isReadonly()\"></textarea>\n </mat-form-field>\n </div>\n </div>\n\n <div class=\"editor-row-card\">\n <div class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.loadingTitle', 'Loading title') }}</mat-label>\n <input matInput [ngModel]=\"stateTitle('loading')\" (ngModelChange)=\"setStateTitle('loading', $event)\" [disabled]=\"isReadonly()\" />\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.loadingDescription', 'Loading description') }}</mat-label>\n <textarea matInput rows=\"2\" [ngModel]=\"stateDescription('loading')\" (ngModelChange)=\"setStateDescription('loading', $event)\" [disabled]=\"isReadonly()\"></textarea>\n </mat-form-field>\n </div>\n </div>\n\n <div class=\"editor-row-card\">\n <div class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.errorTitle', 'Error title') }}</mat-label>\n <input matInput [ngModel]=\"stateTitle('error')\" (ngModelChange)=\"setStateTitle('error', $event)\" [disabled]=\"isReadonly()\" />\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.errorDescription', 'Error description') }}</mat-label>\n <textarea matInput rows=\"2\" [ngModel]=\"stateDescription('error')\" (ngModelChange)=\"setStateDescription('error', $event)\" [disabled]=\"isReadonly()\"></textarea>\n </mat-form-field>\n </div>\n </div>\n </mat-card-content>\n </mat-card>\n </div>\n }\n\n @case ('analytics') {\n <div class=\"editor-stack\">\n @if (showComboPanel()) {\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.specialization.comboTitle', 'Combo guidance') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content>\n <p class=\"editor-caption\">\n {{ t('praxis.charts.editor.specialization.comboHint', 'Combo charts require at least two metrics and allow per-metric axis and series kind mapping.') }}\n </p>\n </mat-card-content>\n </mat-card>\n }\n\n @if (showPieDonutPanel()) {\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.specialization.pieDonutTitle', 'Composition guidance') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content>\n <p class=\"editor-caption\">\n {{ t('praxis.charts.editor.specialization.pieDonutHint', 'Pie and donut charts keep only the first metric and use the first dimension as the category segment.') }}\n </p>\n </mat-card-content>\n </mat-card>\n }\n\n @if (showScatterPanel()) {\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.specialization.scatterTitle', 'Scatter guidance') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content>\n <p class=\"editor-caption\">\n {{ t('praxis.charts.editor.specialization.scatterHint', 'Scatter charts use the first dimension as X and the first metric as Y, so keep both fields mapped.') }}\n </p>\n </mat-card-content>\n </mat-card>\n }\n\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.analytics.dimensionsTitle', 'Dimensions') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-stack\">\n @for (dimension of dimensions(); track $index) {\n <div class=\"editor-row-card\">\n <div class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.dimension', 'Dimension') }}</mat-label>\n @if (fieldOptions('dimension').length) {\n <mat-select [ngModel]=\"dimension.field || ''\" (ngModelChange)=\"setDimensionField($index, $event)\" [disabled]=\"isReadonly()\">\n @for (field of fieldOptions('dimension'); track field.field) {\n <mat-option [value]=\"field.field\">{{ field.label || field.field }}</mat-option>\n }\n </mat-select>\n } @else {\n <input matInput [ngModel]=\"dimension.field || ''\" (ngModelChange)=\"setDimensionField($index, $event)\" [disabled]=\"isReadonly()\" />\n }\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\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\" (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\">\n <div class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.metric', 'Metric') }}</mat-label>\n @if (fieldOptions('metric').length) {\n <mat-select [ngModel]=\"metric.field || ''\" (ngModelChange)=\"setMetricField($index, $event)\" [disabled]=\"isReadonly()\">\n @for (field of fieldOptions('metric'); track field.field) {\n <mat-option [value]=\"field.field\">{{ field.label || field.field }}</mat-option>\n }\n </mat-select>\n } @else {\n <input matInput [ngModel]=\"metric.field || ''\" (ngModelChange)=\"setMetricField($index, $event)\" [disabled]=\"isReadonly()\" />\n }\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\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\">\n <mat-label>{{ t('praxis.charts.editor.field.metricAggregation', 'Aggregation') }}</mat-label>\n <mat-select [ngModel]=\"metric.aggregation || 'sum'\" (ngModelChange)=\"setMetricAggregation($index, $event)\" [disabled]=\"isReadonly()\">\n @for (aggregation of metricAggregations; track aggregation) {\n <mat-option [value]=\"aggregation\">\n {{ t('praxis.charts.editor.metricAggregation.' + aggregation, aggregation) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n @if (showMetricAxisControls()) {\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\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\">\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\" (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\" (click)=\"addMetric()\" [disabled]=\"isReadonly()\">\n {{ t('praxis.charts.editor.analytics.addMetric', 'Add metric') }}\n </button>\n </div>\n </mat-card-content>\n </mat-card>\n </div>\n }\n\n @case ('events') {\n <div class=\"editor-stack\">\n <mat-card class=\"editor-card\">\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 [ngModel]=\"eventAction('pointClick')\" (ngModelChange)=\"setEventAction('pointClick', $event)\" [disabled]=\"isReadonly()\">\n <mat-option value=\"\">{{ t('praxis.charts.editor.events.none', 'None') }}</mat-option>\n @for (action of eventActionOptions; track action) {\n <mat-option [value]=\"action\">\n {{ t('praxis.charts.editor.eventAction.' + action, action) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventTarget', 'Target') }}</mat-label>\n @if (targetOptions().length) {\n <mat-select [ngModel]=\"eventTarget('pointClick')\" (ngModelChange)=\"setEventTarget('pointClick', $event)\" [disabled]=\"isReadonly() || !eventAction('pointClick')\">\n @for (target of targetOptions(); track target.id) {\n <mat-option [value]=\"target.id\">{{ target.label }}</mat-option>\n }\n </mat-select>\n } @else {\n <input matInput [ngModel]=\"eventTarget('pointClick')\" (ngModelChange)=\"setEventTarget('pointClick', $event)\" [disabled]=\"isReadonly() || !eventAction('pointClick')\" />\n }\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventMapping', 'Mapping') }}</mat-label>\n <textarea\n matInput\n 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\">\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 [ngModel]=\"eventAction('drillDown')\" (ngModelChange)=\"setEventAction('drillDown', $event)\" [disabled]=\"isReadonly()\">\n <mat-option value=\"\">{{ t('praxis.charts.editor.events.none', 'None') }}</mat-option>\n @for (action of eventActionOptions; track action) {\n <mat-option [value]=\"action\">\n {{ t('praxis.charts.editor.eventAction.' + action, action) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventTarget', 'Target') }}</mat-label>\n @if (targetOptions().length) {\n <mat-select [ngModel]=\"eventTarget('drillDown')\" (ngModelChange)=\"setEventTarget('drillDown', $event)\" [disabled]=\"isReadonly() || !eventAction('drillDown')\">\n @for (target of targetOptions(); track target.id) {\n <mat-option [value]=\"target.id\">{{ target.label }}</mat-option>\n }\n </mat-select>\n } @else {\n <input matInput [ngModel]=\"eventTarget('drillDown')\" (ngModelChange)=\"setEventTarget('drillDown', $event)\" [disabled]=\"isReadonly() || !eventAction('drillDown')\" />\n }\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventMapping', 'Mapping') }}</mat-label>\n <textarea\n matInput\n 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 </div>\n }\n\n @case ('preview') {\n <p class=\"editor-caption\">\n {{ t('praxis.charts.editor.preview.caption', 'Local preview derived from the canonical contract without remote calls.') }}\n </p>\n }\n }\n </mat-card-content>\n </mat-card>\n </div>\n\n <div class=\"editor-side\">\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.issues.title', 'Validation issues') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content>\n @if (issues().length) {\n <ul class=\"editor-issues\">\n @for (issue of issues(); track issueTrackBy($index, issue)) {\n <li class=\"editor-issue\">\n <strong>{{ issue.field }}</strong>\n <span>{{ issue.message }}</span>\n </li>\n }\n </ul>\n } @else {\n <div class=\"editor-empty\">\n {{ t('praxis.charts.editor.issues.empty', 'No issues were identified.') }}\n </div>\n }\n </mat-card-content>\n </mat-card>\n\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.preview.title', 'Chart preview') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content>\n @if (preview(); as chartPreview) {\n <p class=\"editor-caption\">\n {{ t('praxis.charts.editor.preview.caption', 'Local preview derived from the canonical contract without remote calls.') }}\n </p>\n <praxis-chart [config]=\"chartPreview.config\" [data]=\"chartPreview.data\"></praxis-chart>\n } @else {\n <div class=\"editor-empty\">\n {{ t('praxis.charts.editor.preview.invalid', 'Preview is unavailable while the contract has blocking errors.') }}\n </div>\n }\n </mat-card-content>\n </mat-card>\n </div>\n </div>\n</div>\n", styles: [":host{display:block;min-width:0;color:var(--md-sys-color-on-surface, #1a1b20)}.editor-shell{display:grid;gap:18px}.editor-nav{display:flex;gap:8px;flex-wrap:wrap}.editor-nav button.active{background:color-mix(in srgb,var(--md-sys-color-primary, #1263b4) 18%,transparent);color:var(--md-sys-color-primary, #1263b4)}.editor-layout{display:grid;gap:18px;grid-template-columns:minmax(0,1.35fr) minmax(320px,.9fr);align-items:start}.editor-form,.editor-side{display:grid;gap:16px}.editor-card{border-radius:20px;border:1px solid color-mix(in srgb,var(--md-sys-color-outline, #c5c7ce) 72%,transparent);background:linear-gradient(180deg,#1263b408,#1263b400)}.editor-grid{display:grid;gap:14px;grid-template-columns:repeat(auto-fit,minmax(220px,1fr))}.editor-stack{display:grid;gap:14px}.editor-row-card{padding:14px;border-radius:16px;border:1px solid color-mix(in srgb,var(--md-sys-color-outline, #c5c7ce) 54%,transparent);background:color-mix(in srgb,var(--md-sys-color-surface, #fff) 92%,rgba(18,99,180,.04))}.editor-row-actions{display:flex;justify-content:flex-end}.editor-field{width:100%}.editor-issues{display:grid;gap:10px;margin:0;padding:0;list-style:none}.editor-issue{padding:12px 14px;border-radius:14px;background:color-mix(in srgb,var(--md-sys-color-error, #b3261e) 8%,transparent);border:1px solid color-mix(in srgb,var(--md-sys-color-error, #b3261e) 18%,transparent)}.editor-issue strong{display:block;margin-bottom:4px}.editor-caption{margin:0 0 12px;color:var(--md-sys-color-on-surface-variant, #5a5d67);font-size:.92rem}.editor-empty{padding:18px;border-radius:14px;background:color-mix(in srgb,var(--md-sys-color-surface-variant, #eceff4) 78%,transparent)}@media(max-width:960px){.editor-layout{grid-template-columns:minmax(0,1fr)}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$3.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$3.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$3.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i1$1.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: MatCardModule }, { kind: "component", type: i3$1.MatCard, selector: "mat-card", inputs: ["appearance"], exportAs: ["matCard"] }, { kind: "directive", type: i3$1.MatCardContent, selector: "mat-card-content" }, { kind: "component", type: i3$1.MatCardHeader, selector: "mat-card-header" }, { kind: "directive", type: i3$1.MatCardTitle, selector: "mat-card-title, [mat-card-title], [matCardTitle]" }, { kind: "ngmodule", type: MatFormFieldModule }, { kind: "component", type: i4.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i4.MatLabel, selector: "mat-label" }, { kind: "directive", type: i4.MatHint, selector: "mat-hint", inputs: ["align", "id"] }, { kind: "ngmodule", type: MatInputModule }, { kind: "directive", type: i5.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["disabled", "id", "placeholder", "name", "required", "type", "errorStateMatcher", "aria-describedby", "value", "readonly", "disabledInteractive"], exportAs: ["matInput"] }, { kind: "ngmodule", type: MatSelectModule }, { kind: "component", type: i6.MatSelect, selector: "mat-select", inputs: ["aria-describedby", "panelClass", "disabled", "disableRipple", "tabIndex", "hideSingleSelectionIndicator", "placeholder", "required", "multiple", "disableOptionCentering", "compareWith", "value", "aria-label", "aria-labelledby", "errorStateMatcher", "typeaheadDebounceInterval", "sortComparator", "id", "panelWidth", "canSelectNullableOptions"], outputs: ["openedChange", "opened", "closed", "selectionChange", "valueChange"], exportAs: ["matSelect"] }, { kind: "component", type: i6.MatOption, selector: "mat-option", inputs: ["value", "id", "disabled"], outputs: ["onSelectionChange"], exportAs: ["matOption"] }, { kind: "ngmodule", type: MatSlideToggleModule }, { kind: "component", type: i7.MatSlideToggle, selector: "mat-slide-toggle", inputs: ["name", "id", "labelPosition", "aria-label", "aria-labelledby", "aria-describedby", "required", "color", "disabled", "disableRipple", "tabIndex", "checked", "hideIcon", "disabledInteractive"], outputs: ["change", "toggleChange"], exportAs: ["matSlideToggle"] }, { kind: "component", type: PraxisChartComponent, selector: "praxis-chart", inputs: ["config", "data", "chartDocument", "filterCriteria", "queryContext", "enableCustomization", "availableResources", "availableFields", "availableTargets"], outputs: ["pointClick", "queryRequest", "loadStateChange", "chartDocumentApplied", "chartDocumentSaved"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
5254
5875
|
}
|
|
5255
5876
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: PraxisChartConfigEditor, decorators: [{
|
|
5256
5877
|
type: Component,
|
|
@@ -5264,7 +5885,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
|
|
|
5264
5885
|
MatSelectModule,
|
|
5265
5886
|
MatSlideToggleModule,
|
|
5266
5887
|
PraxisChartComponent,
|
|
5267
|
-
], providers: [providePraxisChartsI18n()], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"editor-shell\">\n <div class=\"editor-nav\">\n @for (section of sections; track section.id) {\n <button\n mat-stroked-button\n type=\"button\"\n [class.active]=\"activeSection() === section.id\"\n (click)=\"setSection(section.id)\"\n >\n {{ t(section.labelKey, section.fallback) }}\n </button>\n }\n </div>\n\n <div class=\"editor-layout\">\n <div class=\"editor-form\">\n <mat-card class=\"editor-card\">\n <mat-card-content>\n @switch (activeSection()) {\n @case ('general') {\n <div class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.chartId', 'Chart ID') }}</mat-label>\n <input matInput [ngModel]=\"doc().chartId || ''\" (ngModelChange)=\"setChartId($event)\" [disabled]=\"isReadonly()\" />\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.kind', 'Kind') }}</mat-label>\n <mat-select [ngModel]=\"doc().kind\" (ngModelChange)=\"setKind($event)\" [disabled]=\"isReadonly()\">\n @for (kind of chartKinds; track kind) {\n <mat-option [value]=\"kind\">\n {{ t('praxis.charts.editor.kind.' + kind, kind) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.title', 'Title') }}</mat-label>\n <input matInput [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.height', 'Height') }}</mat-label>\n <input matInput [ngModel]=\"heightValue()\" (ngModelChange)=\"setHeight($event)\" [disabled]=\"isReadonly()\" />\n </mat-form-field>\n </div>\n }\n\n @case ('data') {\n <div class=\"editor-stack\">\n <div class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.sourceKind', 'Source') }}</mat-label>\n <mat-select [ngModel]=\"doc().source.kind\" (ngModelChange)=\"setSourceKind($event)\" [disabled]=\"isReadonly()\">\n @for (sourceKind of sourceKinds; track sourceKind) {\n <mat-option [value]=\"sourceKind\">\n {{ t('praxis.charts.editor.sourceKind.' + (sourceKind === 'praxis.stats' ? 'praxisStats' : 'derived'), sourceKind) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n @if (doc().source.kind === 'praxis.stats') {\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.resource', 'Resource') }}</mat-label>\n @if (resourceOptions().length) {\n <mat-select [ngModel]=\"resourceValue()\" (ngModelChange)=\"setResource($event)\" [disabled]=\"isReadonly()\">\n @for (resource of resourceOptions(); track resource.id) {\n <mat-option [value]=\"resource.path\">{{ resource.label }}</mat-option>\n }\n </mat-select>\n } @else {\n <input matInput [ngModel]=\"resourceValue()\" (ngModelChange)=\"setResource($event)\" [disabled]=\"isReadonly()\" />\n }\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.operation', 'Operation') }}</mat-label>\n <mat-select\n [ngModel]=\"doc().source.operation || 'group-by'\"\n (ngModelChange)=\"setOperation($event)\"\n [disabled]=\"isReadonly()\"\n >\n @for (operation of operations; track operation) {\n <mat-option [value]=\"operation\">\n {{ t('praxis.charts.editor.operation.' + (operation === 'group-by' ? 'groupBy' : operation), operation) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n }\n </div>\n\n @if (showTimeseriesControls()) {\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.specialization.timeseriesTitle', 'Timeseries options') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.granularity', 'Granularity') }}</mat-label>\n <mat-select [ngModel]=\"granularityValue()\" (ngModelChange)=\"setGranularity($event)\" [disabled]=\"isReadonly()\">\n @for (granularity of timeGranularities; track granularity) {\n <mat-option [value]=\"granularity\">\n {{ t('praxis.charts.editor.granularity.' + granularity, granularity) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n <mat-slide-toggle\n [ngModel]=\"fillGapsValue()\"\n (ngModelChange)=\"setFillGaps($event)\"\n [disabled]=\"isReadonly()\"\n >\n {{ t('praxis.charts.editor.field.fillGaps', 'Fill missing intervals') }}\n </mat-slide-toggle>\n </mat-card-content>\n </mat-card>\n }\n\n @if (showDistributionControls()) {\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.specialization.distributionTitle', 'Distribution options') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.distributionMode', 'Distribution mode') }}</mat-label>\n <mat-select [ngModel]=\"distributionModeValue()\" (ngModelChange)=\"setDistributionMode($event)\" [disabled]=\"isReadonly()\">\n @for (mode of distributionModes; track mode) {\n <mat-option [value]=\"mode\">\n {{ t('praxis.charts.editor.distributionMode.' + mode, mode) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.bucketSize', 'Bucket size') }}</mat-label>\n <input matInput [ngModel]=\"bucketSizeValue()\" (ngModelChange)=\"setBucketSize($event)\" [disabled]=\"isReadonly()\" />\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.bucketCount', 'Bucket count') }}</mat-label>\n <input matInput [ngModel]=\"bucketCountValue()\" (ngModelChange)=\"setBucketCount($event)\" [disabled]=\"isReadonly()\" />\n </mat-form-field>\n </mat-card-content>\n </mat-card>\n }\n </div>\n }\n\n @case ('motion') {\n <div class=\"editor-grid\">\n <mat-slide-toggle\n [ngModel]=\"normalizedDocument().motion?.enabled !== false\"\n (ngModelChange)=\"setMotionEnabled($event)\"\n [disabled]=\"isReadonly()\"\n >\n {{ t('praxis.charts.editor.field.motionEnabled', 'Enable animations') }}\n </mat-slide-toggle>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.motionPreset', 'Motion preset') }}</mat-label>\n <mat-select\n [ngModel]=\"normalizedDocument().motion?.preset || 'standard'\"\n (ngModelChange)=\"setMotionPreset($event)\"\n [disabled]=\"isReadonly() || normalizedDocument().motion?.enabled === false\"\n >\n @for (preset of motionPresets; track preset) {\n <mat-option [value]=\"preset\">\n {{ t('praxis.charts.editor.motionPreset.' + preset, preset) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n </div>\n }\n\n @case ('appearance') {\n <div class=\"editor-stack\">\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.appearance.featuresTitle', 'Display features') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-grid\">\n <mat-slide-toggle\n [ngModel]=\"featureEnabled('legend')\"\n (ngModelChange)=\"setFeatureEnabled('legend', $event)\"\n [disabled]=\"isReadonly()\"\n >\n {{ t('praxis.charts.editor.field.legendEnabled', 'Show legend') }}\n </mat-slide-toggle>\n\n <mat-slide-toggle\n [ngModel]=\"featureEnabled('labels')\"\n (ngModelChange)=\"setFeatureEnabled('labels', $event)\"\n [disabled]=\"isReadonly()\"\n >\n {{ t('praxis.charts.editor.field.labelsEnabled', 'Show labels') }}\n </mat-slide-toggle>\n\n <mat-slide-toggle\n [ngModel]=\"featureEnabled('tooltip')\"\n (ngModelChange)=\"setFeatureEnabled('tooltip', $event)\"\n [disabled]=\"isReadonly()\"\n >\n {{ t('praxis.charts.editor.field.tooltipEnabled', 'Show tooltip') }}\n </mat-slide-toggle>\n </mat-card-content>\n </mat-card>\n\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.appearance.paletteTitle', 'Palette') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content>\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.palette', 'Palette colors') }}</mat-label>\n <textarea\n matInput\n rows=\"3\"\n [ngModel]=\"paletteValue()\"\n (ngModelChange)=\"setPalette($event)\"\n [disabled]=\"isReadonly()\"\n ></textarea>\n </mat-form-field>\n <p class=\"editor-caption\">\n {{ t('praxis.charts.editor.appearance.paletteHint', 'Use comma or line separated colors to persist theme.palette as a canonical array.') }}\n </p>\n </mat-card-content>\n </mat-card>\n\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.appearance.statesTitle', 'State messages') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-stack\">\n <div class=\"editor-row-card\">\n <div class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.emptyTitle', 'Empty title') }}</mat-label>\n <input matInput [ngModel]=\"stateTitle('empty')\" (ngModelChange)=\"setStateTitle('empty', $event)\" [disabled]=\"isReadonly()\" />\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.emptyDescription', 'Empty description') }}</mat-label>\n <textarea matInput rows=\"2\" [ngModel]=\"stateDescription('empty')\" (ngModelChange)=\"setStateDescription('empty', $event)\" [disabled]=\"isReadonly()\"></textarea>\n </mat-form-field>\n </div>\n </div>\n\n <div class=\"editor-row-card\">\n <div class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.loadingTitle', 'Loading title') }}</mat-label>\n <input matInput [ngModel]=\"stateTitle('loading')\" (ngModelChange)=\"setStateTitle('loading', $event)\" [disabled]=\"isReadonly()\" />\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.loadingDescription', 'Loading description') }}</mat-label>\n <textarea matInput rows=\"2\" [ngModel]=\"stateDescription('loading')\" (ngModelChange)=\"setStateDescription('loading', $event)\" [disabled]=\"isReadonly()\"></textarea>\n </mat-form-field>\n </div>\n </div>\n\n <div class=\"editor-row-card\">\n <div class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.errorTitle', 'Error title') }}</mat-label>\n <input matInput [ngModel]=\"stateTitle('error')\" (ngModelChange)=\"setStateTitle('error', $event)\" [disabled]=\"isReadonly()\" />\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.errorDescription', 'Error description') }}</mat-label>\n <textarea matInput rows=\"2\" [ngModel]=\"stateDescription('error')\" (ngModelChange)=\"setStateDescription('error', $event)\" [disabled]=\"isReadonly()\"></textarea>\n </mat-form-field>\n </div>\n </div>\n </mat-card-content>\n </mat-card>\n </div>\n }\n\n @case ('analytics') {\n <div class=\"editor-stack\">\n @if (showComboPanel()) {\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.specialization.comboTitle', 'Combo guidance') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content>\n <p class=\"editor-caption\">\n {{ t('praxis.charts.editor.specialization.comboHint', 'Combo charts require at least two metrics and allow per-metric axis and series kind mapping.') }}\n </p>\n </mat-card-content>\n </mat-card>\n }\n\n @if (showPieDonutPanel()) {\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.specialization.pieDonutTitle', 'Composition guidance') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content>\n <p class=\"editor-caption\">\n {{ t('praxis.charts.editor.specialization.pieDonutHint', 'Pie and donut charts keep only the first metric and use the first dimension as the category segment.') }}\n </p>\n </mat-card-content>\n </mat-card>\n }\n\n @if (showScatterPanel()) {\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.specialization.scatterTitle', 'Scatter guidance') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content>\n <p class=\"editor-caption\">\n {{ t('praxis.charts.editor.specialization.scatterHint', 'Scatter charts use the first dimension as X and the first metric as Y, so keep both fields mapped.') }}\n </p>\n </mat-card-content>\n </mat-card>\n }\n\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.analytics.dimensionsTitle', 'Dimensions') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-stack\">\n @for (dimension of dimensions(); track $index) {\n <div class=\"editor-row-card\">\n <div class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.dimension', 'Dimension') }}</mat-label>\n @if (fieldOptions('dimension').length) {\n <mat-select [ngModel]=\"dimension.field || ''\" (ngModelChange)=\"setDimensionField($index, $event)\" [disabled]=\"isReadonly()\">\n @for (field of fieldOptions('dimension'); track field.field) {\n <mat-option [value]=\"field.field\">{{ field.label || field.field }}</mat-option>\n }\n </mat-select>\n } @else {\n <input matInput [ngModel]=\"dimension.field || ''\" (ngModelChange)=\"setDimensionField($index, $event)\" [disabled]=\"isReadonly()\" />\n }\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\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\" (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\">\n <div class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.metric', 'Metric') }}</mat-label>\n @if (fieldOptions('metric').length) {\n <mat-select [ngModel]=\"metric.field || ''\" (ngModelChange)=\"setMetricField($index, $event)\" [disabled]=\"isReadonly()\">\n @for (field of fieldOptions('metric'); track field.field) {\n <mat-option [value]=\"field.field\">{{ field.label || field.field }}</mat-option>\n }\n </mat-select>\n } @else {\n <input matInput [ngModel]=\"metric.field || ''\" (ngModelChange)=\"setMetricField($index, $event)\" [disabled]=\"isReadonly()\" />\n }\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\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\">\n <mat-label>{{ t('praxis.charts.editor.field.metricAggregation', 'Aggregation') }}</mat-label>\n <mat-select [ngModel]=\"metric.aggregation || 'sum'\" (ngModelChange)=\"setMetricAggregation($index, $event)\" [disabled]=\"isReadonly()\">\n @for (aggregation of metricAggregations; track aggregation) {\n <mat-option [value]=\"aggregation\">\n {{ t('praxis.charts.editor.metricAggregation.' + aggregation, aggregation) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n @if (showMetricAxisControls()) {\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\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\">\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\" (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\" (click)=\"addMetric()\" [disabled]=\"isReadonly()\">\n {{ t('praxis.charts.editor.analytics.addMetric', 'Add metric') }}\n </button>\n </div>\n </mat-card-content>\n </mat-card>\n </div>\n }\n\n @case ('events') {\n <div class=\"editor-stack\">\n <mat-card class=\"editor-card\">\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 [ngModel]=\"eventAction('pointClick')\" (ngModelChange)=\"setEventAction('pointClick', $event)\" [disabled]=\"isReadonly()\">\n <mat-option value=\"\">{{ t('praxis.charts.editor.events.none', 'None') }}</mat-option>\n @for (action of eventActionOptions; track action) {\n <mat-option [value]=\"action\">\n {{ t('praxis.charts.editor.eventAction.' + action, action) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventTarget', 'Target') }}</mat-label>\n @if (targetOptions().length) {\n <mat-select [ngModel]=\"eventTarget('pointClick')\" (ngModelChange)=\"setEventTarget('pointClick', $event)\" [disabled]=\"isReadonly() || !eventAction('pointClick')\">\n @for (target of targetOptions(); track target.id) {\n <mat-option [value]=\"target.id\">{{ target.label }}</mat-option>\n }\n </mat-select>\n } @else {\n <input matInput [ngModel]=\"eventTarget('pointClick')\" (ngModelChange)=\"setEventTarget('pointClick', $event)\" [disabled]=\"isReadonly() || !eventAction('pointClick')\" />\n }\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventMapping', 'Mapping') }}</mat-label>\n <textarea\n matInput\n 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\">\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 [ngModel]=\"eventAction('drillDown')\" (ngModelChange)=\"setEventAction('drillDown', $event)\" [disabled]=\"isReadonly()\">\n <mat-option value=\"\">{{ t('praxis.charts.editor.events.none', 'None') }}</mat-option>\n @for (action of eventActionOptions; track action) {\n <mat-option [value]=\"action\">\n {{ t('praxis.charts.editor.eventAction.' + action, action) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventTarget', 'Target') }}</mat-label>\n @if (targetOptions().length) {\n <mat-select [ngModel]=\"eventTarget('drillDown')\" (ngModelChange)=\"setEventTarget('drillDown', $event)\" [disabled]=\"isReadonly() || !eventAction('drillDown')\">\n @for (target of targetOptions(); track target.id) {\n <mat-option [value]=\"target.id\">{{ target.label }}</mat-option>\n }\n </mat-select>\n } @else {\n <input matInput [ngModel]=\"eventTarget('drillDown')\" (ngModelChange)=\"setEventTarget('drillDown', $event)\" [disabled]=\"isReadonly() || !eventAction('drillDown')\" />\n }\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventMapping', 'Mapping') }}</mat-label>\n <textarea\n matInput\n 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 </div>\n }\n\n @case ('preview') {\n <p class=\"editor-caption\">\n {{ t('praxis.charts.editor.preview.caption', 'Local preview derived from the canonical contract without remote calls.') }}\n </p>\n }\n }\n </mat-card-content>\n </mat-card>\n </div>\n\n <div class=\"editor-side\">\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.issues.title', 'Validation issues') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content>\n @if (issues().length) {\n <ul class=\"editor-issues\">\n @for (issue of issues(); track issueTrackBy($index, issue)) {\n <li class=\"editor-issue\">\n <strong>{{ issue.field }}</strong>\n <span>{{ issue.message }}</span>\n </li>\n }\n </ul>\n } @else {\n <div class=\"editor-empty\">\n {{ t('praxis.charts.editor.issues.empty', 'No issues were identified.') }}\n </div>\n }\n </mat-card-content>\n </mat-card>\n\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.preview.title', 'Chart preview') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content>\n @if (preview(); as chartPreview) {\n <p class=\"editor-caption\">\n {{ t('praxis.charts.editor.preview.caption', 'Local preview derived from the canonical contract without remote calls.') }}\n </p>\n <praxis-chart [config]=\"chartPreview.config\" [data]=\"chartPreview.data\"></praxis-chart>\n } @else {\n <div class=\"editor-empty\">\n {{ t('praxis.charts.editor.preview.invalid', 'Preview is unavailable while the contract has blocking errors.') }}\n </div>\n }\n </mat-card-content>\n </mat-card>\n </div>\n </div>\n</div>\n", styles: [":host{display:block;min-width:0;color:var(--md-sys-color-on-surface, #1a1b20)}.editor-shell{display:grid;gap:18px}.editor-nav{display:flex;gap:8px;flex-wrap:wrap}.editor-nav button.active{background:color-mix(in srgb,var(--md-sys-color-primary, #1263b4) 18%,transparent);color:var(--md-sys-color-primary, #1263b4)}.editor-layout{display:grid;gap:18px;grid-template-columns:minmax(0,1.35fr) minmax(320px,.9fr);align-items:start}.editor-form,.editor-side{display:grid;gap:16px}.editor-card{border-radius:20px;border:1px solid color-mix(in srgb,var(--md-sys-color-outline, #c5c7ce) 72%,transparent);background:linear-gradient(180deg,#1263b408,#1263b400)}.editor-grid{display:grid;gap:14px;grid-template-columns:repeat(auto-fit,minmax(220px,1fr))}.editor-stack{display:grid;gap:14px}.editor-row-card{padding:14px;border-radius:16px;border:1px solid color-mix(in srgb,var(--md-sys-color-outline, #c5c7ce) 54%,transparent);background:color-mix(in srgb,var(--md-sys-color-surface, #fff) 92%,rgba(18,99,180,.04))}.editor-row-actions{display:flex;justify-content:flex-end}.editor-field{width:100%}.editor-issues{display:grid;gap:10px;margin:0;padding:0;list-style:none}.editor-issue{padding:12px 14px;border-radius:14px;background:color-mix(in srgb,var(--md-sys-color-error, #b3261e) 8%,transparent);border:1px solid color-mix(in srgb,var(--md-sys-color-error, #b3261e) 18%,transparent)}.editor-issue strong{display:block;margin-bottom:4px}.editor-caption{margin:0 0 12px;color:var(--md-sys-color-on-surface-variant, #5a5d67);font-size:.92rem}.editor-empty{padding:18px;border-radius:14px;background:color-mix(in srgb,var(--md-sys-color-surface-variant, #eceff4) 78%,transparent)}@media(max-width:960px){.editor-layout{grid-template-columns:minmax(0,1fr)}}\n"] }]
|
|
5888
|
+
], providers: [providePraxisChartsI18n()], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"editor-shell\">\n <div class=\"editor-nav\">\n @for (section of sections; track section.id) {\n <button\n mat-stroked-button\n type=\"button\"\n [class.active]=\"activeSection() === section.id\"\n (click)=\"setSection(section.id)\"\n >\n {{ t(section.labelKey, section.fallback) }}\n </button>\n }\n </div>\n\n <div class=\"editor-layout\">\n <div class=\"editor-form\">\n <mat-card class=\"editor-card\">\n <mat-card-content>\n @switch (activeSection()) {\n @case ('general') {\n <div class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.chartId', 'Chart ID') }}</mat-label>\n <input matInput [ngModel]=\"doc().chartId || ''\" (ngModelChange)=\"setChartId($event)\" [disabled]=\"isReadonly()\" />\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.kind', 'Kind') }}</mat-label>\n <mat-select [ngModel]=\"doc().kind\" (ngModelChange)=\"setKind($event)\" [disabled]=\"isReadonly()\">\n @for (kind of chartKinds; track kind) {\n <mat-option [value]=\"kind\">\n {{ t('praxis.charts.editor.kind.' + kind, kind) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.title', 'Title') }}</mat-label>\n <input matInput [ngModel]=\"titleValue()\" (ngModelChange)=\"setTitle($event)\" [disabled]=\"isReadonly()\" />\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.subtitle', 'Subtitle') }}</mat-label>\n <input matInput [ngModel]=\"subtitleValue()\" (ngModelChange)=\"setSubtitle($event)\" [disabled]=\"isReadonly()\" />\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.sizingMode', 'Sizing') }}</mat-label>\n <mat-select [ngModel]=\"sizingModeValue()\" (ngModelChange)=\"setSizingMode($event)\" [disabled]=\"isReadonly()\">\n @for (mode of sizingModes; track mode) {\n <mat-option [value]=\"mode\">\n {{ t('praxis.charts.editor.sizing.' + mode, mode) }}\n </mat-option>\n }\n </mat-select>\n <mat-hint>{{ t('praxis.charts.editor.hint.sizingMode', 'Use fill-container only when the host widget provides a defined body height.') }}</mat-hint>\n </mat-form-field>\n\n @if (sizingModeValue() === 'fixed') {\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.height', 'Height') }}</mat-label>\n <input matInput [ngModel]=\"heightValue()\" (ngModelChange)=\"setSizingHeight($event)\" [disabled]=\"isReadonly()\" />\n <mat-hint>{{ t('praxis.charts.editor.hint.height', 'Numbers are saved as pixels; CSS lengths such as 20rem are also accepted.') }}</mat-hint>\n </mat-form-field>\n }\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.minHeight', 'Minimum height') }}</mat-label>\n <input matInput [ngModel]=\"sizingMinHeightValue()\" (ngModelChange)=\"setSizingMinHeight($event)\" [disabled]=\"isReadonly()\" />\n <mat-hint>{{ t('praxis.charts.editor.hint.minHeight', 'Set a readable minimum for compact dashboard widgets.') }}</mat-hint>\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.maxHeight', 'Maximum height') }}</mat-label>\n <input matInput [ngModel]=\"sizingMaxHeightValue()\" (ngModelChange)=\"setSizingMaxHeight($event)\" [disabled]=\"isReadonly()\" />\n <mat-hint>{{ t('praxis.charts.editor.hint.maxHeight', 'Leave empty unless the chart must stop growing inside a flexible layout.') }}</mat-hint>\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.aspectRatio', 'Aspect ratio') }}</mat-label>\n <input matInput [ngModel]=\"sizingAspectRatioValue()\" (ngModelChange)=\"setSizingAspectRatio($event)\" [disabled]=\"isReadonly()\" />\n <mat-hint>{{ t('praxis.charts.editor.hint.aspectRatio', 'Optional. Use values such as 1.777 or 16 / 9.') }}</mat-hint>\n </mat-form-field>\n </div>\n }\n\n @case ('data') {\n <div class=\"editor-stack\">\n <div class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.sourceKind', 'Source') }}</mat-label>\n <mat-select [ngModel]=\"doc().source.kind\" (ngModelChange)=\"setSourceKind($event)\" [disabled]=\"isReadonly()\">\n @for (sourceKind of sourceKinds; track sourceKind) {\n <mat-option [value]=\"sourceKind\">\n {{ t('praxis.charts.editor.sourceKind.' + (sourceKind === 'praxis.stats' ? 'praxisStats' : 'derived'), sourceKind) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n @if (doc().source.kind === 'praxis.stats') {\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.resource', 'Resource') }}</mat-label>\n @if (resourceOptions().length) {\n <mat-select [ngModel]=\"resourceValue()\" (ngModelChange)=\"setResource($event)\" [disabled]=\"isReadonly()\">\n @for (resource of resourceOptions(); track resource.id) {\n <mat-option [value]=\"resource.path\">{{ resource.label }}</mat-option>\n }\n </mat-select>\n } @else {\n <input matInput [ngModel]=\"resourceValue()\" (ngModelChange)=\"setResource($event)\" [disabled]=\"isReadonly()\" />\n }\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.operation', 'Operation') }}</mat-label>\n <mat-select\n [ngModel]=\"doc().source.operation || 'group-by'\"\n (ngModelChange)=\"setOperation($event)\"\n [disabled]=\"isReadonly()\"\n >\n @for (operation of operations; track operation) {\n <mat-option [value]=\"operation\">\n {{ t('praxis.charts.editor.operation.' + (operation === 'group-by' ? 'groupBy' : operation), operation) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n }\n </div>\n\n @if (showTimeseriesControls()) {\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.specialization.timeseriesTitle', 'Timeseries options') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.granularity', 'Granularity') }}</mat-label>\n <mat-select [ngModel]=\"granularityValue()\" (ngModelChange)=\"setGranularity($event)\" [disabled]=\"isReadonly()\">\n @for (granularity of timeGranularities; track granularity) {\n <mat-option [value]=\"granularity\">\n {{ t('praxis.charts.editor.granularity.' + granularity, granularity) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n <mat-slide-toggle\n [ngModel]=\"fillGapsValue()\"\n (ngModelChange)=\"setFillGaps($event)\"\n [disabled]=\"isReadonly()\"\n >\n {{ t('praxis.charts.editor.field.fillGaps', 'Fill missing intervals') }}\n </mat-slide-toggle>\n </mat-card-content>\n </mat-card>\n }\n\n @if (showDistributionControls()) {\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.specialization.distributionTitle', 'Distribution options') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.distributionMode', 'Distribution mode') }}</mat-label>\n <mat-select [ngModel]=\"distributionModeValue()\" (ngModelChange)=\"setDistributionMode($event)\" [disabled]=\"isReadonly()\">\n @for (mode of distributionModes; track mode) {\n <mat-option [value]=\"mode\">\n {{ t('praxis.charts.editor.distributionMode.' + mode, mode) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.bucketSize', 'Bucket size') }}</mat-label>\n <input matInput [ngModel]=\"bucketSizeValue()\" (ngModelChange)=\"setBucketSize($event)\" [disabled]=\"isReadonly()\" />\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.bucketCount', 'Bucket count') }}</mat-label>\n <input matInput [ngModel]=\"bucketCountValue()\" (ngModelChange)=\"setBucketCount($event)\" [disabled]=\"isReadonly()\" />\n </mat-form-field>\n </mat-card-content>\n </mat-card>\n }\n </div>\n }\n\n @case ('motion') {\n <div class=\"editor-grid\">\n <mat-slide-toggle\n [ngModel]=\"normalizedDocument().motion?.enabled !== false\"\n (ngModelChange)=\"setMotionEnabled($event)\"\n [disabled]=\"isReadonly()\"\n >\n {{ t('praxis.charts.editor.field.motionEnabled', 'Enable animations') }}\n </mat-slide-toggle>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.motionPreset', 'Motion preset') }}</mat-label>\n <mat-select\n [ngModel]=\"normalizedDocument().motion?.preset || 'standard'\"\n (ngModelChange)=\"setMotionPreset($event)\"\n [disabled]=\"isReadonly() || normalizedDocument().motion?.enabled === false\"\n >\n @for (preset of motionPresets; track preset) {\n <mat-option [value]=\"preset\">\n {{ t('praxis.charts.editor.motionPreset.' + preset, preset) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n </div>\n }\n\n @case ('appearance') {\n <div class=\"editor-stack\">\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.appearance.featuresTitle', 'Display features') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-grid\">\n <mat-slide-toggle\n [ngModel]=\"featureEnabled('legend')\"\n (ngModelChange)=\"setFeatureEnabled('legend', $event)\"\n [disabled]=\"isReadonly()\"\n >\n {{ t('praxis.charts.editor.field.legendEnabled', 'Show legend') }}\n </mat-slide-toggle>\n\n <mat-slide-toggle\n [ngModel]=\"featureEnabled('labels')\"\n (ngModelChange)=\"setFeatureEnabled('labels', $event)\"\n [disabled]=\"isReadonly()\"\n >\n {{ t('praxis.charts.editor.field.labelsEnabled', 'Show labels') }}\n </mat-slide-toggle>\n\n <mat-slide-toggle\n [ngModel]=\"featureEnabled('tooltip')\"\n (ngModelChange)=\"setFeatureEnabled('tooltip', $event)\"\n [disabled]=\"isReadonly()\"\n >\n {{ t('praxis.charts.editor.field.tooltipEnabled', 'Show tooltip') }}\n </mat-slide-toggle>\n </mat-card-content>\n </mat-card>\n\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.appearance.paletteTitle', 'Palette') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-stack\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.themeVariant', 'Theme variant') }}</mat-label>\n <mat-select [ngModel]=\"themeVariantValue()\" (ngModelChange)=\"setThemeVariant($event)\" [disabled]=\"isReadonly()\">\n <mat-option [value]=\"''\">\n {{ t('praxis.charts.editor.themeVariant.none', 'No variant') }}\n </mat-option>\n @for (variant of themeVariants; track variant) {\n <mat-option [value]=\"variant\">\n {{ t('praxis.charts.editor.themeVariant.' + variant, variant) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.paletteMode', 'Palette mode') }}</mat-label>\n <mat-select [ngModel]=\"paletteModeValue()\" (ngModelChange)=\"setPaletteMode($event)\" [disabled]=\"isReadonly()\">\n @for (mode of paletteModes; track mode) {\n <mat-option [value]=\"mode\">\n {{ t('praxis.charts.editor.paletteMode.' + mode, mode) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n @if (paletteModeValue() === 'token') {\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.paletteToken', 'Palette token') }}</mat-label>\n <mat-select [ngModel]=\"paletteTokenValue()\" (ngModelChange)=\"setPaletteToken($event)\" [disabled]=\"isReadonly()\">\n @for (token of paletteTokens; track token) {\n <mat-option [value]=\"token\">\n {{ t('praxis.charts.editor.paletteToken.' + token, token) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n } @else {\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.palette', 'Palette colors') }}</mat-label>\n <textarea\n matInput\n rows=\"3\"\n [ngModel]=\"paletteValue()\"\n (ngModelChange)=\"setPalette($event)\"\n [disabled]=\"isReadonly()\"\n ></textarea>\n </mat-form-field>\n }\n\n <p class=\"editor-caption\">\n {{ t('praxis.charts.editor.appearance.paletteHint', 'Use a registered token or comma-separated colors to persist theme.palette in the canonical contract.') }}\n </p>\n </mat-card-content>\n </mat-card>\n\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.appearance.surfaceTitle', 'Surface') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content>\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.surfaceMode', 'Surface mode') }}</mat-label>\n <mat-select [ngModel]=\"surfaceModeValue()\" (ngModelChange)=\"setSurfaceMode($event)\" [disabled]=\"isReadonly()\">\n @for (mode of surfaceModes; track mode) {\n <mat-option [value]=\"mode\">\n {{ t('praxis.charts.editor.surface.' + mode, mode) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n <p class=\"editor-caption\">\n {{ t('praxis.charts.editor.appearance.surfaceHint', 'Use embedded for dashboard widgets and contained only when the chart must own its visual surface.') }}\n </p>\n </mat-card-content>\n </mat-card>\n\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.appearance.statesTitle', 'State messages') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-stack\">\n <div class=\"editor-row-card\">\n <div class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.emptyTitle', 'Empty title') }}</mat-label>\n <input matInput [ngModel]=\"stateTitle('empty')\" (ngModelChange)=\"setStateTitle('empty', $event)\" [disabled]=\"isReadonly()\" />\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.emptyDescription', 'Empty description') }}</mat-label>\n <textarea matInput rows=\"2\" [ngModel]=\"stateDescription('empty')\" (ngModelChange)=\"setStateDescription('empty', $event)\" [disabled]=\"isReadonly()\"></textarea>\n </mat-form-field>\n </div>\n </div>\n\n <div class=\"editor-row-card\">\n <div class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.loadingTitle', 'Loading title') }}</mat-label>\n <input matInput [ngModel]=\"stateTitle('loading')\" (ngModelChange)=\"setStateTitle('loading', $event)\" [disabled]=\"isReadonly()\" />\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.loadingDescription', 'Loading description') }}</mat-label>\n <textarea matInput rows=\"2\" [ngModel]=\"stateDescription('loading')\" (ngModelChange)=\"setStateDescription('loading', $event)\" [disabled]=\"isReadonly()\"></textarea>\n </mat-form-field>\n </div>\n </div>\n\n <div class=\"editor-row-card\">\n <div class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.errorTitle', 'Error title') }}</mat-label>\n <input matInput [ngModel]=\"stateTitle('error')\" (ngModelChange)=\"setStateTitle('error', $event)\" [disabled]=\"isReadonly()\" />\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.errorDescription', 'Error description') }}</mat-label>\n <textarea matInput rows=\"2\" [ngModel]=\"stateDescription('error')\" (ngModelChange)=\"setStateDescription('error', $event)\" [disabled]=\"isReadonly()\"></textarea>\n </mat-form-field>\n </div>\n </div>\n </mat-card-content>\n </mat-card>\n </div>\n }\n\n @case ('analytics') {\n <div class=\"editor-stack\">\n @if (showComboPanel()) {\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.specialization.comboTitle', 'Combo guidance') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content>\n <p class=\"editor-caption\">\n {{ t('praxis.charts.editor.specialization.comboHint', 'Combo charts require at least two metrics and allow per-metric axis and series kind mapping.') }}\n </p>\n </mat-card-content>\n </mat-card>\n }\n\n @if (showPieDonutPanel()) {\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.specialization.pieDonutTitle', 'Composition guidance') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content>\n <p class=\"editor-caption\">\n {{ t('praxis.charts.editor.specialization.pieDonutHint', 'Pie and donut charts keep only the first metric and use the first dimension as the category segment.') }}\n </p>\n </mat-card-content>\n </mat-card>\n }\n\n @if (showScatterPanel()) {\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.specialization.scatterTitle', 'Scatter guidance') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content>\n <p class=\"editor-caption\">\n {{ t('praxis.charts.editor.specialization.scatterHint', 'Scatter charts use the first dimension as X and the first metric as Y, so keep both fields mapped.') }}\n </p>\n </mat-card-content>\n </mat-card>\n }\n\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.analytics.dimensionsTitle', 'Dimensions') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-stack\">\n @for (dimension of dimensions(); track $index) {\n <div class=\"editor-row-card\">\n <div class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.dimension', 'Dimension') }}</mat-label>\n @if (fieldOptions('dimension').length) {\n <mat-select [ngModel]=\"dimension.field || ''\" (ngModelChange)=\"setDimensionField($index, $event)\" [disabled]=\"isReadonly()\">\n @for (field of fieldOptions('dimension'); track field.field) {\n <mat-option [value]=\"field.field\">{{ field.label || field.field }}</mat-option>\n }\n </mat-select>\n } @else {\n <input matInput [ngModel]=\"dimension.field || ''\" (ngModelChange)=\"setDimensionField($index, $event)\" [disabled]=\"isReadonly()\" />\n }\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\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\" (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\">\n <div class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.metric', 'Metric') }}</mat-label>\n @if (fieldOptions('metric').length) {\n <mat-select [ngModel]=\"metric.field || ''\" (ngModelChange)=\"setMetricField($index, $event)\" [disabled]=\"isReadonly()\">\n @for (field of fieldOptions('metric'); track field.field) {\n <mat-option [value]=\"field.field\">{{ field.label || field.field }}</mat-option>\n }\n </mat-select>\n } @else {\n <input matInput [ngModel]=\"metric.field || ''\" (ngModelChange)=\"setMetricField($index, $event)\" [disabled]=\"isReadonly()\" />\n }\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\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\">\n <mat-label>{{ t('praxis.charts.editor.field.metricAggregation', 'Aggregation') }}</mat-label>\n <mat-select [ngModel]=\"metric.aggregation || 'sum'\" (ngModelChange)=\"setMetricAggregation($index, $event)\" [disabled]=\"isReadonly()\">\n @for (aggregation of metricAggregations; track aggregation) {\n <mat-option [value]=\"aggregation\">\n {{ t('praxis.charts.editor.metricAggregation.' + aggregation, aggregation) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n @if (showMetricAxisControls()) {\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\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\">\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\" (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\" (click)=\"addMetric()\" [disabled]=\"isReadonly()\">\n {{ t('praxis.charts.editor.analytics.addMetric', 'Add metric') }}\n </button>\n </div>\n </mat-card-content>\n </mat-card>\n </div>\n }\n\n @case ('events') {\n <div class=\"editor-stack\">\n <mat-card class=\"editor-card\">\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 [ngModel]=\"eventAction('pointClick')\" (ngModelChange)=\"setEventAction('pointClick', $event)\" [disabled]=\"isReadonly()\">\n <mat-option value=\"\">{{ t('praxis.charts.editor.events.none', 'None') }}</mat-option>\n @for (action of eventActionOptions; track action) {\n <mat-option [value]=\"action\">\n {{ t('praxis.charts.editor.eventAction.' + action, action) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventTarget', 'Target') }}</mat-label>\n @if (targetOptions().length) {\n <mat-select [ngModel]=\"eventTarget('pointClick')\" (ngModelChange)=\"setEventTarget('pointClick', $event)\" [disabled]=\"isReadonly() || !eventAction('pointClick')\">\n @for (target of targetOptions(); track target.id) {\n <mat-option [value]=\"target.id\">{{ target.label }}</mat-option>\n }\n </mat-select>\n } @else {\n <input matInput [ngModel]=\"eventTarget('pointClick')\" (ngModelChange)=\"setEventTarget('pointClick', $event)\" [disabled]=\"isReadonly() || !eventAction('pointClick')\" />\n }\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventMapping', 'Mapping') }}</mat-label>\n <textarea\n matInput\n 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\">\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 [ngModel]=\"eventAction('drillDown')\" (ngModelChange)=\"setEventAction('drillDown', $event)\" [disabled]=\"isReadonly()\">\n <mat-option value=\"\">{{ t('praxis.charts.editor.events.none', 'None') }}</mat-option>\n @for (action of eventActionOptions; track action) {\n <mat-option [value]=\"action\">\n {{ t('praxis.charts.editor.eventAction.' + action, action) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventTarget', 'Target') }}</mat-label>\n @if (targetOptions().length) {\n <mat-select [ngModel]=\"eventTarget('drillDown')\" (ngModelChange)=\"setEventTarget('drillDown', $event)\" [disabled]=\"isReadonly() || !eventAction('drillDown')\">\n @for (target of targetOptions(); track target.id) {\n <mat-option [value]=\"target.id\">{{ target.label }}</mat-option>\n }\n </mat-select>\n } @else {\n <input matInput [ngModel]=\"eventTarget('drillDown')\" (ngModelChange)=\"setEventTarget('drillDown', $event)\" [disabled]=\"isReadonly() || !eventAction('drillDown')\" />\n }\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventMapping', 'Mapping') }}</mat-label>\n <textarea\n matInput\n 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 </div>\n }\n\n @case ('preview') {\n <p class=\"editor-caption\">\n {{ t('praxis.charts.editor.preview.caption', 'Local preview derived from the canonical contract without remote calls.') }}\n </p>\n }\n }\n </mat-card-content>\n </mat-card>\n </div>\n\n <div class=\"editor-side\">\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.issues.title', 'Validation issues') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content>\n @if (issues().length) {\n <ul class=\"editor-issues\">\n @for (issue of issues(); track issueTrackBy($index, issue)) {\n <li class=\"editor-issue\">\n <strong>{{ issue.field }}</strong>\n <span>{{ issue.message }}</span>\n </li>\n }\n </ul>\n } @else {\n <div class=\"editor-empty\">\n {{ t('praxis.charts.editor.issues.empty', 'No issues were identified.') }}\n </div>\n }\n </mat-card-content>\n </mat-card>\n\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.preview.title', 'Chart preview') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content>\n @if (preview(); as chartPreview) {\n <p class=\"editor-caption\">\n {{ t('praxis.charts.editor.preview.caption', 'Local preview derived from the canonical contract without remote calls.') }}\n </p>\n <praxis-chart [config]=\"chartPreview.config\" [data]=\"chartPreview.data\"></praxis-chart>\n } @else {\n <div class=\"editor-empty\">\n {{ t('praxis.charts.editor.preview.invalid', 'Preview is unavailable while the contract has blocking errors.') }}\n </div>\n }\n </mat-card-content>\n </mat-card>\n </div>\n </div>\n</div>\n", styles: [":host{display:block;min-width:0;color:var(--md-sys-color-on-surface, #1a1b20)}.editor-shell{display:grid;gap:18px}.editor-nav{display:flex;gap:8px;flex-wrap:wrap}.editor-nav button.active{background:color-mix(in srgb,var(--md-sys-color-primary, #1263b4) 18%,transparent);color:var(--md-sys-color-primary, #1263b4)}.editor-layout{display:grid;gap:18px;grid-template-columns:minmax(0,1.35fr) minmax(320px,.9fr);align-items:start}.editor-form,.editor-side{display:grid;gap:16px}.editor-card{border-radius:20px;border:1px solid color-mix(in srgb,var(--md-sys-color-outline, #c5c7ce) 72%,transparent);background:linear-gradient(180deg,#1263b408,#1263b400)}.editor-grid{display:grid;gap:14px;grid-template-columns:repeat(auto-fit,minmax(220px,1fr))}.editor-stack{display:grid;gap:14px}.editor-row-card{padding:14px;border-radius:16px;border:1px solid color-mix(in srgb,var(--md-sys-color-outline, #c5c7ce) 54%,transparent);background:color-mix(in srgb,var(--md-sys-color-surface, #fff) 92%,rgba(18,99,180,.04))}.editor-row-actions{display:flex;justify-content:flex-end}.editor-field{width:100%}.editor-issues{display:grid;gap:10px;margin:0;padding:0;list-style:none}.editor-issue{padding:12px 14px;border-radius:14px;background:color-mix(in srgb,var(--md-sys-color-error, #b3261e) 8%,transparent);border:1px solid color-mix(in srgb,var(--md-sys-color-error, #b3261e) 18%,transparent)}.editor-issue strong{display:block;margin-bottom:4px}.editor-caption{margin:0 0 12px;color:var(--md-sys-color-on-surface-variant, #5a5d67);font-size:.92rem}.editor-empty{padding:18px;border-radius:14px;background:color-mix(in srgb,var(--md-sys-color-surface-variant, #eceff4) 78%,transparent)}@media(max-width:960px){.editor-layout{grid-template-columns:minmax(0,1fr)}}\n"] }]
|
|
5268
5889
|
}], ctorParameters: () => [], propDecorators: { documentInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "document", required: false }] }], modeInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "mode", required: false }] }], readonlyInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "readonly", required: false }] }], availableResourcesInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "availableResources", required: false }] }], availableFieldsInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "availableFields", required: false }] }], availableTargetsInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "availableTargets", required: false }] }], apply: [{ type: i0.Output, args: ["apply"] }], save: [{ type: i0.Output, args: ["save"] }], resetChange: [{ type: i0.Output, args: ["resetChange"] }], documentChange: [{ type: i0.Output, args: ["documentChange"] }] } });
|
|
5269
5890
|
|
|
5270
5891
|
var praxisChartConfigEditor = /*#__PURE__*/Object.freeze({
|
|
@@ -5280,4 +5901,4 @@ var praxisChartConfigEditor = /*#__PURE__*/Object.freeze({
|
|
|
5280
5901
|
* Generated bundle index. Do not edit.
|
|
5281
5902
|
*/
|
|
5282
5903
|
|
|
5283
|
-
export { AnalyticsChartConfigAdapterService, AnalyticsChartContractService, ChartContractNormalizerService, ChartContractValidationService, ChartEditorDefaultsService, ChartEditorPreviewMapperService, PRAXIS_CHARTS_I18N, PRAXIS_CHART_BACKEND_MOCK_BAR, PRAXIS_CHART_BACKEND_MOCK_COMBO, PRAXIS_CHART_BACKEND_MOCK_DONUT, PRAXIS_CHART_BACKEND_MOCK_HORIZONTAL_BAR, PRAXIS_CHART_BACKEND_MOCK_MULTI_METRIC_BAR, PRAXIS_CHART_BACKEND_MOCK_SCATTER, PRAXIS_CHART_BACKEND_MOCK_STACKED_AREA, PRAXIS_CHART_BACKEND_MOCK_TIMESERIES, PRAXIS_CHART_COMPONENT_METADATA, PRAXIS_CHART_DRILLDOWN_DATA_BY_MONTH, PRAXIS_CHART_DRILLDOWN_PANEL_METADATA, PRAXIS_CHART_ENGINE, PRAXIS_CHART_STATE_PROBE_COMPONENT_METADATA, PraxisChartBackendPayloadAdapterService, PraxisChartCanonicalContractMapperService, PraxisChartComponent, PraxisChartCompositionShowcaseComponent, PraxisChartConfigEditor, PraxisChartDataTransformerService, PraxisChartDrilldownPanelComponent, PraxisChartOptionBuilderService, PraxisChartSchemaMapperService, PraxisChartStateProbeComponent, PraxisChartStatsApiService, buildPraxisChartInteractiveCanvasPage, buildPraxisChartInteractiveWidgetPage, buildPraxisChartMockCanvasPage, buildPraxisChartMockWidgetPage, createPraxisChartsI18nConfig, providePraxisChartDrilldownPanelMetadata, providePraxisChartStateProbeMetadata, providePraxisCharts, providePraxisChartsI18n, providePraxisChartsMetadata, resolvePraxisChartsText };
|
|
5904
|
+
export { AnalyticsChartConfigAdapterService, AnalyticsChartContractService, ChartContractNormalizerService, ChartContractValidationService, ChartEditorDefaultsService, ChartEditorPreviewMapperService, PRAXIS_CHARTS_I18N, PRAXIS_CHART_BACKEND_MOCK_BAR, PRAXIS_CHART_BACKEND_MOCK_COMBO, PRAXIS_CHART_BACKEND_MOCK_DONUT, PRAXIS_CHART_BACKEND_MOCK_HORIZONTAL_BAR, PRAXIS_CHART_BACKEND_MOCK_MULTI_METRIC_BAR, PRAXIS_CHART_BACKEND_MOCK_SCATTER, PRAXIS_CHART_BACKEND_MOCK_STACKED_AREA, PRAXIS_CHART_BACKEND_MOCK_TIMESERIES, PRAXIS_CHART_COMPONENT_METADATA, PRAXIS_CHART_DRILLDOWN_DATA_BY_MONTH, PRAXIS_CHART_DRILLDOWN_PANEL_METADATA, PRAXIS_CHART_ENGINE, PRAXIS_CHART_PALETTE_TOKENS, PRAXIS_CHART_STATE_PROBE_COMPONENT_METADATA, PRAXIS_CHART_THEME_VARIANTS, PraxisChartBackendPayloadAdapterService, PraxisChartCanonicalContractMapperService, PraxisChartComponent, PraxisChartCompositionShowcaseComponent, PraxisChartConfigEditor, PraxisChartDataTransformerService, PraxisChartDrilldownPanelComponent, PraxisChartOptionBuilderService, PraxisChartSchemaMapperService, PraxisChartStateProbeComponent, PraxisChartStatsApiService, buildPraxisChartInteractiveCanvasPage, buildPraxisChartInteractiveWidgetPage, buildPraxisChartMockCanvasPage, buildPraxisChartMockWidgetPage, createPraxisChartsI18nConfig, isPraxisChartPaletteToken, providePraxisChartDrilldownPanelMetadata, providePraxisChartStateProbeMetadata, providePraxisCharts, providePraxisChartsI18n, providePraxisChartsMetadata, resolvePraxisChartPaletteToken, resolvePraxisChartsText };
|