@praxisui/charts 7.0.0-beta.0 → 8.0.0-beta.1
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 +130 -19
- package/fesm2022/praxisui-charts.mjs +918 -115
- package/index.d.ts +146 -17
- package/package.json +3 -3
|
@@ -4,6 +4,7 @@ import { Injectable, Inject, InjectionToken, input, booleanAttribute, output, vi
|
|
|
4
4
|
import * as i1$2 from '@praxisui/core';
|
|
5
5
|
import { buildApiUrl, API_URL, PraxisI18nService, SETTINGS_PANEL_BRIDGE, ComponentMetadataRegistry, createDefaultTableConfig, AnalyticsStatsRequestBuilderService, DynamicWidgetPageComponent, providePraxisI18n, SETTINGS_PANEL_DATA } from '@praxisui/core';
|
|
6
6
|
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
|
7
|
+
import { throwError, map, of, isObservable, from, BehaviorSubject } from 'rxjs';
|
|
7
8
|
import * as i1$1 from '@angular/material/button';
|
|
8
9
|
import { MatButtonModule } from '@angular/material/button';
|
|
9
10
|
import * as i2 from '@angular/material/icon';
|
|
@@ -16,7 +17,6 @@ import { AriaComponent, DatasetComponent, GridComponent, LegendComponent, TitleC
|
|
|
16
17
|
import { CanvasRenderer } from 'echarts/renderers';
|
|
17
18
|
import * as i1 from '@angular/common/http';
|
|
18
19
|
import { HttpErrorResponse } from '@angular/common/http';
|
|
19
|
-
import { throwError, map, BehaviorSubject } from 'rxjs';
|
|
20
20
|
import { catchError } from 'rxjs/operators';
|
|
21
21
|
import { PraxisTable } from '@praxisui/table';
|
|
22
22
|
import * as i1$3 from '@angular/forms';
|
|
@@ -187,6 +187,13 @@ class PraxisChartDataTransformerService {
|
|
|
187
187
|
if (aggregation === 'count') {
|
|
188
188
|
return rows.length;
|
|
189
189
|
}
|
|
190
|
+
if (aggregation === 'distinct-count') {
|
|
191
|
+
const field = seriesConfig.metric?.field;
|
|
192
|
+
if (!field) {
|
|
193
|
+
return 0;
|
|
194
|
+
}
|
|
195
|
+
return new Set(rows.map((row) => this.normalizeCategory(row[field]))).size;
|
|
196
|
+
}
|
|
190
197
|
const values = rows
|
|
191
198
|
.map((row) => this.extractMetricValue(row, seriesConfig))
|
|
192
199
|
.filter((value) => Number.isFinite(value));
|
|
@@ -255,6 +262,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
|
|
|
255
262
|
}] });
|
|
256
263
|
|
|
257
264
|
const PRAXIS_CHART_DEFAULT_PALETTE = ['#1263b4', '#0f766e', '#f08c00', '#c92a2a', '#7b61ff'];
|
|
265
|
+
const TITLE_TOP = 18;
|
|
266
|
+
const TITLE_LEFT = 24;
|
|
267
|
+
const CARTESIAN_GRID_TOP_WITH_TITLE = 96;
|
|
268
|
+
const CARTESIAN_GRID_TOP_WITHOUT_TITLE = 48;
|
|
269
|
+
const CARTESIAN_GRID_BOTTOM_WITH_LEGEND = 64;
|
|
270
|
+
const CARTESIAN_GRID_BOTTOM_WITHOUT_LEGEND = 40;
|
|
258
271
|
class PraxisChartOptionBuilderService {
|
|
259
272
|
transformer;
|
|
260
273
|
constructor(transformer) {
|
|
@@ -266,12 +279,21 @@ class PraxisChartOptionBuilderService {
|
|
|
266
279
|
const tooltipEnabled = config.theme?.tooltip?.enabled ?? true;
|
|
267
280
|
const legendVisible = config.theme?.legend?.visible ?? true;
|
|
268
281
|
if (transformed.mode === 'pie') {
|
|
282
|
+
const hasChartTitle = this.hasText(config.title) || this.hasText(config.subtitle);
|
|
283
|
+
const labelsVisible = config.series[0]?.labels?.visible ?? false;
|
|
269
284
|
const pieSeries = {
|
|
270
285
|
type: 'pie',
|
|
271
|
-
radius: config.type === 'donut'
|
|
286
|
+
radius: config.type === 'donut'
|
|
287
|
+
? labelsVisible ? ['32%', '52%'] : ['42%', '64%']
|
|
288
|
+
: labelsVisible ? '54%' : '66%',
|
|
289
|
+
center: ['50%', hasChartTitle ? '54%' : '50%'],
|
|
290
|
+
avoidLabelOverlap: true,
|
|
272
291
|
label: {
|
|
273
|
-
show:
|
|
292
|
+
show: labelsVisible,
|
|
293
|
+
overflow: 'truncate',
|
|
294
|
+
width: 120,
|
|
274
295
|
},
|
|
296
|
+
labelLine: labelsVisible ? { length: 14, length2: 12 } : undefined,
|
|
275
297
|
data: transformed.slices.map((slice) => ({
|
|
276
298
|
name: slice.name,
|
|
277
299
|
value: slice.value,
|
|
@@ -281,15 +303,9 @@ class PraxisChartOptionBuilderService {
|
|
|
281
303
|
return {
|
|
282
304
|
backgroundColor: config.theme?.backgroundColor,
|
|
283
305
|
color: palette,
|
|
284
|
-
title:
|
|
285
|
-
text: this.resolveText(config.title),
|
|
286
|
-
subtext: this.resolveText(config.subtitle),
|
|
287
|
-
left: 'center',
|
|
288
|
-
},
|
|
306
|
+
title: this.buildTitle(config, 'center'),
|
|
289
307
|
tooltip: tooltipEnabled ? { trigger: 'item' } : undefined,
|
|
290
|
-
legend: legendVisible
|
|
291
|
-
? { show: true, orient: 'horizontal', bottom: 0 }
|
|
292
|
-
: { show: false },
|
|
308
|
+
legend: this.buildLegend(config, legendVisible, 'bottom'),
|
|
293
309
|
series: [pieSeries],
|
|
294
310
|
};
|
|
295
311
|
}
|
|
@@ -305,29 +321,31 @@ class PraxisChartOptionBuilderService {
|
|
|
305
321
|
return {
|
|
306
322
|
backgroundColor: config.theme?.backgroundColor,
|
|
307
323
|
color: palette,
|
|
308
|
-
title:
|
|
309
|
-
text: this.resolveText(config.title),
|
|
310
|
-
subtext: this.resolveText(config.subtitle),
|
|
311
|
-
left: 'left',
|
|
312
|
-
},
|
|
324
|
+
title: this.buildTitle(config, 'left'),
|
|
313
325
|
tooltip: tooltipEnabled ? { trigger: 'item' } : undefined,
|
|
314
|
-
legend: legendVisible
|
|
315
|
-
grid: this.buildGrid(),
|
|
326
|
+
legend: this.buildLegend(config, legendVisible, 'bottom'),
|
|
327
|
+
grid: this.buildGrid(config, { legendVisible }),
|
|
316
328
|
xAxis: {
|
|
317
329
|
type: config.axes?.x?.type ?? 'value',
|
|
318
330
|
name: config.axes?.x?.label,
|
|
331
|
+
nameLocation: 'middle',
|
|
332
|
+
nameGap: 32,
|
|
319
333
|
axisLabel: {
|
|
320
334
|
show: config.axes?.x?.labels?.visible ?? true,
|
|
321
335
|
rotate: config.axes?.x?.labels?.rotate ?? 0,
|
|
336
|
+
hideOverlap: true,
|
|
322
337
|
},
|
|
323
338
|
},
|
|
324
339
|
yAxis: {
|
|
325
340
|
type: config.axes?.y?.type ?? 'value',
|
|
326
341
|
name: config.axes?.y?.label,
|
|
342
|
+
nameLocation: 'middle',
|
|
343
|
+
nameGap: 40,
|
|
327
344
|
min: config.axes?.y?.min,
|
|
328
345
|
max: config.axes?.y?.max,
|
|
329
346
|
axisLabel: {
|
|
330
347
|
show: config.axes?.y?.labels?.visible ?? true,
|
|
348
|
+
hideOverlap: true,
|
|
331
349
|
},
|
|
332
350
|
},
|
|
333
351
|
series: scatterSeries,
|
|
@@ -337,35 +355,39 @@ class PraxisChartOptionBuilderService {
|
|
|
337
355
|
return {
|
|
338
356
|
backgroundColor: config.theme?.backgroundColor,
|
|
339
357
|
color: palette,
|
|
340
|
-
title:
|
|
341
|
-
text: this.resolveText(config.title),
|
|
342
|
-
subtext: this.resolveText(config.subtitle),
|
|
343
|
-
left: 'left',
|
|
344
|
-
},
|
|
358
|
+
title: this.buildTitle(config, 'left'),
|
|
345
359
|
tooltip: tooltipEnabled
|
|
346
|
-
? {
|
|
360
|
+
? {
|
|
361
|
+
trigger: config.theme?.tooltip?.trigger ?? 'axis',
|
|
362
|
+
confine: true,
|
|
363
|
+
appendToBody: false,
|
|
364
|
+
}
|
|
347
365
|
: undefined,
|
|
348
|
-
legend: legendVisible
|
|
349
|
-
|
|
350
|
-
: { show: false },
|
|
351
|
-
grid: this.buildGrid(),
|
|
366
|
+
legend: this.buildLegend(config, legendVisible, 'bottom'),
|
|
367
|
+
grid: this.buildGrid(config, { horizontal, legendVisible }),
|
|
352
368
|
xAxis: horizontal
|
|
353
369
|
? {
|
|
354
370
|
type: config.axes?.y?.type ?? 'value',
|
|
355
371
|
name: config.axes?.y?.label,
|
|
372
|
+
nameLocation: 'middle',
|
|
373
|
+
nameGap: 32,
|
|
356
374
|
min: config.axes?.y?.min,
|
|
357
375
|
max: config.axes?.y?.max,
|
|
358
376
|
axisLabel: {
|
|
359
377
|
show: config.axes?.y?.labels?.visible ?? true,
|
|
378
|
+
hideOverlap: true,
|
|
360
379
|
},
|
|
361
380
|
}
|
|
362
381
|
: {
|
|
363
382
|
type: config.axes?.x?.type ?? 'category',
|
|
364
383
|
name: config.axes?.x?.label,
|
|
365
384
|
data: transformed.categories,
|
|
385
|
+
nameLocation: 'middle',
|
|
386
|
+
nameGap: 18,
|
|
366
387
|
axisLabel: {
|
|
367
388
|
show: config.axes?.x?.labels?.visible ?? true,
|
|
368
389
|
rotate: config.axes?.x?.labels?.rotate ?? 0,
|
|
390
|
+
hideOverlap: true,
|
|
369
391
|
},
|
|
370
392
|
},
|
|
371
393
|
yAxis: horizontal
|
|
@@ -376,6 +398,9 @@ class PraxisChartOptionBuilderService {
|
|
|
376
398
|
axisLabel: {
|
|
377
399
|
show: config.axes?.x?.labels?.visible ?? true,
|
|
378
400
|
rotate: config.axes?.x?.labels?.rotate ?? 0,
|
|
401
|
+
hideOverlap: true,
|
|
402
|
+
overflow: 'truncate',
|
|
403
|
+
width: 160,
|
|
379
404
|
},
|
|
380
405
|
}
|
|
381
406
|
: this.buildCartesianYAxis(config),
|
|
@@ -402,14 +427,97 @@ class PraxisChartOptionBuilderService {
|
|
|
402
427
|
const text = value.text;
|
|
403
428
|
return text || undefined;
|
|
404
429
|
}
|
|
430
|
+
if (typeof value === 'object' && value && 'fallback' in value) {
|
|
431
|
+
const fallback = value.fallback;
|
|
432
|
+
return fallback || undefined;
|
|
433
|
+
}
|
|
405
434
|
return undefined;
|
|
406
435
|
}
|
|
407
|
-
|
|
436
|
+
hasText(value) {
|
|
437
|
+
const text = this.resolveText(value);
|
|
438
|
+
return !!text && text.trim().length > 0;
|
|
439
|
+
}
|
|
440
|
+
buildTitle(config, align) {
|
|
441
|
+
const title = this.resolveText(config.title);
|
|
442
|
+
const subtitle = this.resolveText(config.subtitle);
|
|
443
|
+
const hasTitle = !!title || !!subtitle;
|
|
408
444
|
return {
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
445
|
+
show: hasTitle,
|
|
446
|
+
text: title,
|
|
447
|
+
subtext: subtitle,
|
|
448
|
+
top: TITLE_TOP,
|
|
449
|
+
left: align === 'center' ? 'center' : TITLE_LEFT,
|
|
450
|
+
right: TITLE_LEFT,
|
|
451
|
+
textStyle: {
|
|
452
|
+
fontSize: 18,
|
|
453
|
+
fontWeight: 600,
|
|
454
|
+
lineHeight: 24,
|
|
455
|
+
color: config.theme?.textColor,
|
|
456
|
+
overflow: 'truncate',
|
|
457
|
+
},
|
|
458
|
+
subtextStyle: {
|
|
459
|
+
fontSize: 12,
|
|
460
|
+
lineHeight: 18,
|
|
461
|
+
color: config.theme?.textColor,
|
|
462
|
+
overflow: 'truncate',
|
|
463
|
+
},
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
buildLegend(config, visible, fallbackPosition) {
|
|
467
|
+
if (!visible) {
|
|
468
|
+
return { show: false };
|
|
469
|
+
}
|
|
470
|
+
const position = config.theme?.legend?.position ?? fallbackPosition;
|
|
471
|
+
const base = {
|
|
472
|
+
show: true,
|
|
473
|
+
type: 'scroll',
|
|
474
|
+
pageIconSize: 10,
|
|
475
|
+
itemWidth: 18,
|
|
476
|
+
itemHeight: 10,
|
|
477
|
+
textStyle: {
|
|
478
|
+
color: config.theme?.textColor ?? '#4b5563',
|
|
479
|
+
},
|
|
480
|
+
formatter: (name) => truncateLegendText(name),
|
|
481
|
+
};
|
|
482
|
+
if (position === 'top') {
|
|
483
|
+
return {
|
|
484
|
+
...base,
|
|
485
|
+
orient: 'horizontal',
|
|
486
|
+
top: this.hasText(config.title) || this.hasText(config.subtitle) ? 70 : 12,
|
|
487
|
+
left: 'center',
|
|
488
|
+
right: TITLE_LEFT,
|
|
489
|
+
};
|
|
490
|
+
}
|
|
491
|
+
if (position === 'left' || position === 'right') {
|
|
492
|
+
return {
|
|
493
|
+
...base,
|
|
494
|
+
orient: 'vertical',
|
|
495
|
+
top: 88,
|
|
496
|
+
bottom: 28,
|
|
497
|
+
[position]: 10,
|
|
498
|
+
};
|
|
499
|
+
}
|
|
500
|
+
return {
|
|
501
|
+
...base,
|
|
502
|
+
orient: 'horizontal',
|
|
503
|
+
bottom: 16,
|
|
504
|
+
left: TITLE_LEFT,
|
|
505
|
+
right: TITLE_LEFT,
|
|
506
|
+
};
|
|
507
|
+
}
|
|
508
|
+
buildGrid(config, options = {}) {
|
|
509
|
+
const hasChartTitle = this.hasText(config.title) || this.hasText(config.subtitle);
|
|
510
|
+
const legendPosition = config.theme?.legend?.position ?? 'bottom';
|
|
511
|
+
const sideLegend = options.legendVisible && (legendPosition === 'left' || legendPosition === 'right');
|
|
512
|
+
return {
|
|
513
|
+
top: hasChartTitle ? CARTESIAN_GRID_TOP_WITH_TITLE : CARTESIAN_GRID_TOP_WITHOUT_TITLE,
|
|
514
|
+
right: sideLegend && legendPosition === 'right' ? 160 : 32,
|
|
515
|
+
bottom: options.legendVisible && legendPosition === 'bottom'
|
|
516
|
+
? CARTESIAN_GRID_BOTTOM_WITH_LEGEND
|
|
517
|
+
: CARTESIAN_GRID_BOTTOM_WITHOUT_LEGEND,
|
|
518
|
+
left: sideLegend && legendPosition === 'left'
|
|
519
|
+
? 168
|
|
520
|
+
: options.horizontal ? 136 : 56,
|
|
413
521
|
// ECharts deprecou containLabel; esta combinacao preserva o mesmo comportamento
|
|
414
522
|
// sem depender da feature legacy de grid.
|
|
415
523
|
outerBoundsMode: 'same',
|
|
@@ -420,10 +528,13 @@ class PraxisChartOptionBuilderService {
|
|
|
420
528
|
const primaryAxis = {
|
|
421
529
|
type: config.axes?.y?.type ?? 'value',
|
|
422
530
|
name: config.axes?.y?.label,
|
|
531
|
+
nameLocation: 'middle',
|
|
532
|
+
nameGap: 40,
|
|
423
533
|
min: config.axes?.y?.min,
|
|
424
534
|
max: config.axes?.y?.max,
|
|
425
535
|
axisLabel: {
|
|
426
536
|
show: config.axes?.y?.labels?.visible ?? true,
|
|
537
|
+
hideOverlap: true,
|
|
427
538
|
},
|
|
428
539
|
};
|
|
429
540
|
if (!config.axes?.ySecondary) {
|
|
@@ -434,11 +545,14 @@ class PraxisChartOptionBuilderService {
|
|
|
434
545
|
{
|
|
435
546
|
type: config.axes.ySecondary.type ?? 'value',
|
|
436
547
|
name: config.axes.ySecondary.label,
|
|
548
|
+
nameLocation: 'middle',
|
|
549
|
+
nameGap: 40,
|
|
437
550
|
min: config.axes.ySecondary.min,
|
|
438
551
|
max: config.axes.ySecondary.max,
|
|
439
552
|
position: config.axes.ySecondary.position ?? 'right',
|
|
440
553
|
axisLabel: {
|
|
441
554
|
show: config.axes.ySecondary.labels?.visible ?? true,
|
|
555
|
+
hideOverlap: true,
|
|
442
556
|
},
|
|
443
557
|
},
|
|
444
558
|
];
|
|
@@ -450,6 +564,13 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
|
|
|
450
564
|
type: Injectable,
|
|
451
565
|
args: [{ providedIn: 'root' }]
|
|
452
566
|
}], ctorParameters: () => [{ type: PraxisChartDataTransformerService }] });
|
|
567
|
+
function truncateLegendText(name) {
|
|
568
|
+
const value = String(name ?? '').trim();
|
|
569
|
+
if (value.length <= 28) {
|
|
570
|
+
return value;
|
|
571
|
+
}
|
|
572
|
+
return `${value.slice(0, 25)}...`;
|
|
573
|
+
}
|
|
453
574
|
|
|
454
575
|
use([
|
|
455
576
|
AriaComponent,
|
|
@@ -503,10 +624,59 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
|
|
|
503
624
|
type: Injectable
|
|
504
625
|
}], ctorParameters: () => [{ type: PraxisChartOptionBuilderService }] });
|
|
505
626
|
|
|
627
|
+
const PRAXIS_CHART_PALETTE_TOKENS = {
|
|
628
|
+
'brand-primary': ['#1263b4', '#0f766e', '#f08c00', '#c92a2a', '#7b61ff'],
|
|
629
|
+
'brand-balanced': ['#1263b4', '#15803d', '#7c3aed', '#c2410c', '#be123c'],
|
|
630
|
+
status: ['#15803d', '#ca8a04', '#dc2626', '#2563eb', '#7c3aed'],
|
|
631
|
+
executive: ['#14b8a6', '#f59e0b', '#e11d48', '#8b5cf6', '#38bdf8'],
|
|
632
|
+
};
|
|
633
|
+
function isPraxisChartPaletteToken(value) {
|
|
634
|
+
return Object.prototype.hasOwnProperty.call(PRAXIS_CHART_PALETTE_TOKENS, value);
|
|
635
|
+
}
|
|
636
|
+
function resolvePraxisChartPaletteToken(value) {
|
|
637
|
+
return isPraxisChartPaletteToken(value)
|
|
638
|
+
? [...PRAXIS_CHART_PALETTE_TOKENS[value]]
|
|
639
|
+
: undefined;
|
|
640
|
+
}
|
|
641
|
+
const PRAXIS_CHART_THEME_VARIANTS = {
|
|
642
|
+
default: {
|
|
643
|
+
palette: [...PRAXIS_CHART_PALETTE_TOKENS['brand-primary']],
|
|
644
|
+
legend: { position: 'bottom' },
|
|
645
|
+
surface: { mode: 'auto' },
|
|
646
|
+
},
|
|
647
|
+
compact: {
|
|
648
|
+
palette: [...PRAXIS_CHART_PALETTE_TOKENS['brand-balanced']],
|
|
649
|
+
textColor: '#374151',
|
|
650
|
+
borderRadius: 6,
|
|
651
|
+
legend: { position: 'bottom' },
|
|
652
|
+
surface: {
|
|
653
|
+
mode: 'embedded',
|
|
654
|
+
background: 'transparent',
|
|
655
|
+
borderWidth: 0,
|
|
656
|
+
borderRadius: 0,
|
|
657
|
+
},
|
|
658
|
+
},
|
|
659
|
+
executive: {
|
|
660
|
+
palette: [...PRAXIS_CHART_PALETTE_TOKENS.executive],
|
|
661
|
+
backgroundColor: '#111827',
|
|
662
|
+
textColor: '#f9fafb',
|
|
663
|
+
borderRadius: 8,
|
|
664
|
+
legend: { position: 'bottom' },
|
|
665
|
+
surface: {
|
|
666
|
+
mode: 'contained',
|
|
667
|
+
background: '#111827',
|
|
668
|
+
borderColor: '#374151',
|
|
669
|
+
borderWidth: 1,
|
|
670
|
+
borderRadius: 8,
|
|
671
|
+
},
|
|
672
|
+
},
|
|
673
|
+
};
|
|
674
|
+
|
|
506
675
|
class ChartContractNormalizerService {
|
|
507
676
|
normalize(input) {
|
|
508
677
|
let document = structuredClone(input);
|
|
509
678
|
document = this.normalizeMotion(document);
|
|
679
|
+
document = this.normalizeSizing(document);
|
|
510
680
|
document = this.normalizeKindSpecificFields(document);
|
|
511
681
|
document = this.normalizeOperationSpecificFields(document);
|
|
512
682
|
document = this.normalizeSourceSpecificFields(document);
|
|
@@ -532,6 +702,44 @@ class ChartContractNormalizerService {
|
|
|
532
702
|
},
|
|
533
703
|
};
|
|
534
704
|
}
|
|
705
|
+
normalizeSizing(document) {
|
|
706
|
+
if (!document.sizing && document.height !== undefined) {
|
|
707
|
+
const { height, ...rest } = document;
|
|
708
|
+
return {
|
|
709
|
+
...rest,
|
|
710
|
+
sizing: {
|
|
711
|
+
mode: 'fixed',
|
|
712
|
+
height: this.normalizeCssSizeValue(height),
|
|
713
|
+
},
|
|
714
|
+
};
|
|
715
|
+
}
|
|
716
|
+
if (!document.sizing) {
|
|
717
|
+
return document;
|
|
718
|
+
}
|
|
719
|
+
const mode = document.sizing.mode ?? (document.sizing.height !== undefined ? 'fixed' : 'auto');
|
|
720
|
+
const { height: sizingHeight, ...restSizing } = document.sizing;
|
|
721
|
+
const normalizedSizing = {
|
|
722
|
+
...restSizing,
|
|
723
|
+
minHeight: this.normalizeCssSizeValue(restSizing.minHeight),
|
|
724
|
+
maxHeight: this.normalizeCssSizeValue(restSizing.maxHeight),
|
|
725
|
+
aspectRatio: this.normalizeAspectRatioValue(restSizing.aspectRatio),
|
|
726
|
+
};
|
|
727
|
+
const sizing = mode === 'fixed'
|
|
728
|
+
? {
|
|
729
|
+
...normalizedSizing,
|
|
730
|
+
mode,
|
|
731
|
+
height: this.normalizeCssSizeValue(sizingHeight),
|
|
732
|
+
}
|
|
733
|
+
: {
|
|
734
|
+
...normalizedSizing,
|
|
735
|
+
mode,
|
|
736
|
+
};
|
|
737
|
+
const { height, ...rest } = document;
|
|
738
|
+
return {
|
|
739
|
+
...rest,
|
|
740
|
+
sizing: this.omitUndefined(sizing),
|
|
741
|
+
};
|
|
742
|
+
}
|
|
535
743
|
normalizeKindSpecificFields(document) {
|
|
536
744
|
let nextDocument = document;
|
|
537
745
|
if (document.kind === 'horizontal-bar') {
|
|
@@ -594,6 +802,29 @@ class ChartContractNormalizerService {
|
|
|
594
802
|
isDistribution(document) {
|
|
595
803
|
return document.source.kind === 'praxis.stats' && document.source.operation === 'distribution';
|
|
596
804
|
}
|
|
805
|
+
normalizeCssSizeValue(value) {
|
|
806
|
+
if (typeof value !== 'string') {
|
|
807
|
+
return value;
|
|
808
|
+
}
|
|
809
|
+
const trimmed = value.trim();
|
|
810
|
+
if (!trimmed) {
|
|
811
|
+
return undefined;
|
|
812
|
+
}
|
|
813
|
+
return /^-?\d+(\.\d+)?$/.test(trimmed) ? Number(trimmed) : trimmed;
|
|
814
|
+
}
|
|
815
|
+
normalizeAspectRatioValue(value) {
|
|
816
|
+
if (typeof value !== 'string') {
|
|
817
|
+
return value;
|
|
818
|
+
}
|
|
819
|
+
const trimmed = value.trim();
|
|
820
|
+
if (!trimmed) {
|
|
821
|
+
return undefined;
|
|
822
|
+
}
|
|
823
|
+
return /^\d+(\.\d+)?$/.test(trimmed) ? Number(trimmed) : trimmed;
|
|
824
|
+
}
|
|
825
|
+
omitUndefined(value) {
|
|
826
|
+
return Object.fromEntries(Object.entries(value).filter(([, entryValue]) => entryValue !== undefined));
|
|
827
|
+
}
|
|
597
828
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: ChartContractNormalizerService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
598
829
|
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: ChartContractNormalizerService, providedIn: 'root' });
|
|
599
830
|
}
|
|
@@ -607,6 +838,7 @@ class ChartContractValidationService {
|
|
|
607
838
|
const issues = [];
|
|
608
839
|
this.validateSource(document, issues);
|
|
609
840
|
this.validateTheme(document, issues);
|
|
841
|
+
this.validateSizing(document, issues);
|
|
610
842
|
this.validateMetrics(document, issues);
|
|
611
843
|
this.validateKinds(document, issues);
|
|
612
844
|
this.validateEvents(document, issues);
|
|
@@ -625,37 +857,66 @@ class ChartContractValidationService {
|
|
|
625
857
|
if (document.source.kind === 'praxis.stats' && !document.source.operation) {
|
|
626
858
|
issues.push(this.error('missing-operation', 'source.operation', 'x-ui.chart source.operation is required for source.kind="praxis.stats".'));
|
|
627
859
|
}
|
|
860
|
+
const granularity = document.source.kind === 'praxis.stats'
|
|
861
|
+
? document.source.options?.granularity
|
|
862
|
+
: undefined;
|
|
863
|
+
if (granularity && granularity !== 'day' && granularity !== 'week' && granularity !== 'month') {
|
|
864
|
+
issues.push(this.error('unsupported-granularity', 'source.options.granularity', `x-ui.chart source.options.granularity="${granularity}" is not supported in @praxisui/charts.`));
|
|
865
|
+
}
|
|
628
866
|
}
|
|
629
867
|
validateTheme(document, issues) {
|
|
630
868
|
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.'));
|
|
869
|
+
if (!isPraxisChartPaletteToken(document.theme.palette)) {
|
|
870
|
+
issues.push(this.error('palette-token-unknown', 'theme.palette', `x-ui.chart theme.palette token "${document.theme.palette}" is not registered in @praxisui/charts.`));
|
|
871
|
+
}
|
|
635
872
|
}
|
|
636
873
|
}
|
|
637
874
|
validateMetrics(document, issues) {
|
|
638
875
|
if (!document.metrics?.length) {
|
|
639
|
-
issues.push(this.error('missing-metric', 'metrics', 'x-ui.chart requires at least one metric
|
|
876
|
+
issues.push(this.error('missing-metric', 'metrics', 'x-ui.chart requires at least one metric.'));
|
|
640
877
|
}
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
878
|
+
document.metrics
|
|
879
|
+
?.filter((metric) => metric.aggregation === 'distinct-count' && !metric.field?.trim())
|
|
880
|
+
.forEach((_metric, index) => {
|
|
881
|
+
issues.push(this.error('distinct-count-missing-field', `metrics[${index}].field`, 'x-ui.chart aggregation "distinct-count" requires a metric field.'));
|
|
882
|
+
});
|
|
883
|
+
}
|
|
884
|
+
validateSizing(document, issues) {
|
|
885
|
+
const sizing = document.sizing;
|
|
886
|
+
if (!sizing) {
|
|
887
|
+
return;
|
|
888
|
+
}
|
|
889
|
+
if (sizing.mode
|
|
890
|
+
&& sizing.mode !== 'fixed'
|
|
891
|
+
&& sizing.mode !== 'fill-container'
|
|
892
|
+
&& sizing.mode !== 'auto') {
|
|
893
|
+
issues.push(this.error('sizing-mode-unsupported', 'sizing.mode', `x-ui.chart sizing.mode="${sizing.mode}" is not supported in @praxisui/charts.`));
|
|
894
|
+
}
|
|
895
|
+
if (sizing.mode === 'fixed' && sizing.height === undefined) {
|
|
896
|
+
issues.push(this.error('sizing-fixed-missing-height', 'sizing.height', 'x-ui.chart sizing.height is required when sizing.mode="fixed".'));
|
|
897
|
+
}
|
|
898
|
+
this.validateCssSize('height', sizing.height, 'sizing.height', issues);
|
|
899
|
+
this.validateCssSize('minHeight', sizing.minHeight, 'sizing.minHeight', issues);
|
|
900
|
+
this.validateCssSize('maxHeight', sizing.maxHeight, 'sizing.maxHeight', issues);
|
|
901
|
+
const minHeight = this.numericCssSize(sizing.minHeight);
|
|
902
|
+
const maxHeight = this.numericCssSize(sizing.maxHeight);
|
|
903
|
+
if (minHeight !== null && maxHeight !== null && maxHeight < minHeight) {
|
|
904
|
+
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.'));
|
|
905
|
+
}
|
|
906
|
+
if (sizing.aspectRatio !== undefined && !this.isValidAspectRatio(sizing.aspectRatio)) {
|
|
907
|
+
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
908
|
}
|
|
648
909
|
}
|
|
649
910
|
validateKinds(document, issues) {
|
|
650
911
|
const metricCount = document.metrics?.length ?? 0;
|
|
651
912
|
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
|
|
913
|
+
issues.push(this.error('missing-dimension', 'dimensions', 'x-ui.chart cartesian charts require at least one dimension.'));
|
|
653
914
|
}
|
|
654
915
|
if ((document.kind === 'pie' || document.kind === 'donut') && !document.dimensions?.[0]?.field) {
|
|
655
916
|
issues.push(this.error('pie-missing-dimension', 'dimensions[0].field', 'x-ui.chart pie/donut charts require a first dimension for category mapping.'));
|
|
656
917
|
}
|
|
657
918
|
if ((document.kind === 'pie' || document.kind === 'donut') && metricCount > 1) {
|
|
658
|
-
issues.push(this.error('pie-multi-metric', 'metrics', 'x-ui.chart pie/donut charts
|
|
919
|
+
issues.push(this.error('pie-multi-metric', 'metrics', 'x-ui.chart pie/donut charts support a single metric in @praxisui/charts.'));
|
|
659
920
|
}
|
|
660
921
|
if (document.kind === 'combo' && metricCount < 2) {
|
|
661
922
|
issues.push(this.error('combo-min-metrics', 'metrics', 'x-ui.chart combo charts require at least two metrics.'));
|
|
@@ -666,7 +927,7 @@ class ChartContractValidationService {
|
|
|
666
927
|
if (document.source.kind === 'praxis.stats'
|
|
667
928
|
&& document.source.operation === 'distribution'
|
|
668
929
|
&& metricCount > 1) {
|
|
669
|
-
issues.push(this.error('distribution-single-metric', 'metrics', 'x-ui.chart praxis.stats distribution
|
|
930
|
+
issues.push(this.error('distribution-single-metric', 'metrics', 'x-ui.chart praxis.stats distribution supports only a single metric in @praxisui/charts.'));
|
|
670
931
|
}
|
|
671
932
|
if (document.kind === 'horizontal-bar' && document.orientation && document.orientation !== 'horizontal') {
|
|
672
933
|
issues.push(this.error('horizontal-bar-orientation', 'orientation', 'x-ui.chart kind="horizontal-bar" requires orientation="horizontal" when orientation is provided.'));
|
|
@@ -681,18 +942,14 @@ class ChartContractValidationService {
|
|
|
681
942
|
&& document.source.kind === 'praxis.stats'
|
|
682
943
|
&& document.source.operation !== 'group-by'
|
|
683
944
|
&& document.source.operation !== 'timeseries') {
|
|
684
|
-
issues.push(this.error('combo-operation-unsupported', 'source.operation', 'x-ui.chart combo charts over praxis.stats
|
|
945
|
+
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
946
|
}
|
|
686
947
|
}
|
|
687
948
|
validateEvents(document, issues) {
|
|
688
949
|
this.validateEventAction('pointClick', document.events?.pointClick, issues);
|
|
689
950
|
this.validateEventAction('drillDown', document.events?.drillDown, issues);
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
}
|
|
693
|
-
if (document.events?.crossFilter) {
|
|
694
|
-
issues.push(this.error('cross-filter-unsupported', 'events.crossFilter', 'x-ui.chart selectionChange/crossFilter declarative runtime actions are not yet implemented in @praxisui/charts.'));
|
|
695
|
-
}
|
|
951
|
+
this.validateEventAction('selectionChange', document.events?.selectionChange, issues);
|
|
952
|
+
this.validateEventAction('crossFilter', document.events?.crossFilter, issues);
|
|
696
953
|
}
|
|
697
954
|
validateEventAction(eventKey, eventAction, issues) {
|
|
698
955
|
if (!eventAction?.action) {
|
|
@@ -710,6 +967,43 @@ class ChartContractValidationService {
|
|
|
710
967
|
message,
|
|
711
968
|
};
|
|
712
969
|
}
|
|
970
|
+
validateCssSize(key, value, field, issues) {
|
|
971
|
+
if (value === undefined) {
|
|
972
|
+
return;
|
|
973
|
+
}
|
|
974
|
+
if (!this.isValidCssSize(value)) {
|
|
975
|
+
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)".`));
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
isValidCssSize(value) {
|
|
979
|
+
if (typeof value === 'number') {
|
|
980
|
+
return Number.isFinite(value) && value >= 0;
|
|
981
|
+
}
|
|
982
|
+
const trimmed = value.trim();
|
|
983
|
+
return trimmed.length > 0 && (/^\d+(\.\d+)?$/.test(trimmed)
|
|
984
|
+
|| /^\d+(\.\d+)?(px|rem|em|vh|vw|vmin|vmax|%)$/.test(trimmed)
|
|
985
|
+
|| /^(calc|min|max|clamp)\(.+\)$/.test(trimmed)
|
|
986
|
+
|| /^var\(.+\)$/.test(trimmed));
|
|
987
|
+
}
|
|
988
|
+
numericCssSize(value) {
|
|
989
|
+
if (typeof value === 'number') {
|
|
990
|
+
return Number.isFinite(value) ? value : null;
|
|
991
|
+
}
|
|
992
|
+
const trimmed = value?.trim();
|
|
993
|
+
if (!trimmed) {
|
|
994
|
+
return null;
|
|
995
|
+
}
|
|
996
|
+
const match = /^(\d+(\.\d+)?)(px)?$/.exec(trimmed);
|
|
997
|
+
return match ? Number(match[1]) : null;
|
|
998
|
+
}
|
|
999
|
+
isValidAspectRatio(value) {
|
|
1000
|
+
if (typeof value === 'number') {
|
|
1001
|
+
return Number.isFinite(value) && value > 0;
|
|
1002
|
+
}
|
|
1003
|
+
const trimmed = value.trim();
|
|
1004
|
+
return (/^\d+(\.\d+)?$/.test(trimmed)
|
|
1005
|
+
|| /^\d+(\.\d+)?\s*\/\s*\d+(\.\d+)?$/.test(trimmed));
|
|
1006
|
+
}
|
|
713
1007
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: ChartContractValidationService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
714
1008
|
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: ChartContractValidationService, providedIn: 'root' });
|
|
715
1009
|
}
|
|
@@ -738,7 +1032,7 @@ class PraxisChartCanonicalContractMapperService {
|
|
|
738
1032
|
orientation: this.resolveOrientation(normalizedContract),
|
|
739
1033
|
title: this.mapTextValue(normalizedContract.title),
|
|
740
1034
|
subtitle: this.mapTextValue(normalizedContract.subtitle),
|
|
741
|
-
|
|
1035
|
+
sizing: normalizedContract.sizing,
|
|
742
1036
|
axes: this.buildAxes(normalizedContract),
|
|
743
1037
|
series: this.buildSeries(normalizedContract),
|
|
744
1038
|
dataSource: this.buildDataSource(normalizedContract),
|
|
@@ -874,18 +1168,48 @@ class PraxisChartCanonicalContractMapperService {
|
|
|
874
1168
|
pointClick: Boolean(contract.events?.pointClick || contract.events?.drillDown),
|
|
875
1169
|
selection: Boolean(contract.events?.selectionChange),
|
|
876
1170
|
drillDown: Boolean(contract.events?.drillDown),
|
|
1171
|
+
crossFilter: Boolean(contract.events?.crossFilter),
|
|
1172
|
+
eventActions: {
|
|
1173
|
+
pointClick: contract.events?.pointClick,
|
|
1174
|
+
selectionChange: contract.events?.selectionChange,
|
|
1175
|
+
drillDown: contract.events?.drillDown,
|
|
1176
|
+
crossFilter: contract.events?.crossFilter,
|
|
1177
|
+
},
|
|
877
1178
|
};
|
|
878
1179
|
}
|
|
879
1180
|
buildTheme(contract) {
|
|
1181
|
+
const variantTheme = contract.theme?.variant
|
|
1182
|
+
? PRAXIS_CHART_THEME_VARIANTS[contract.theme.variant]
|
|
1183
|
+
: undefined;
|
|
1184
|
+
const palette = this.resolveThemePalette(contract);
|
|
880
1185
|
return {
|
|
881
|
-
|
|
882
|
-
|
|
1186
|
+
...variantTheme,
|
|
1187
|
+
palette: palette ?? variantTheme?.palette,
|
|
1188
|
+
legend: {
|
|
1189
|
+
...(variantTheme?.legend ?? {}),
|
|
1190
|
+
visible: this.resolveToggle(contract.legend, true),
|
|
1191
|
+
},
|
|
883
1192
|
tooltip: {
|
|
1193
|
+
...(variantTheme?.tooltip ?? {}),
|
|
884
1194
|
enabled: this.resolveToggle(contract.tooltip, true),
|
|
885
1195
|
trigger: contract.kind === 'pie' || contract.kind === 'donut' || contract.kind === 'scatter' ? 'item' : 'axis',
|
|
886
1196
|
},
|
|
1197
|
+
surface: {
|
|
1198
|
+
...(variantTheme?.surface ?? {}),
|
|
1199
|
+
...(contract.theme?.surface ?? {}),
|
|
1200
|
+
},
|
|
1201
|
+
backgroundColor: contract.theme?.surface?.background ?? variantTheme?.backgroundColor,
|
|
1202
|
+
textColor: variantTheme?.textColor,
|
|
1203
|
+
borderRadius: variantTheme?.borderRadius,
|
|
887
1204
|
};
|
|
888
1205
|
}
|
|
1206
|
+
resolveThemePalette(contract) {
|
|
1207
|
+
const palette = contract.theme?.palette;
|
|
1208
|
+
if (Array.isArray(palette)) {
|
|
1209
|
+
return palette;
|
|
1210
|
+
}
|
|
1211
|
+
return palette ? resolvePraxisChartPaletteToken(palette) : undefined;
|
|
1212
|
+
}
|
|
889
1213
|
buildMotion(motion) {
|
|
890
1214
|
if (!motion) {
|
|
891
1215
|
return undefined;
|
|
@@ -943,12 +1267,13 @@ class PraxisChartCanonicalContractMapperService {
|
|
|
943
1267
|
case 'min':
|
|
944
1268
|
case 'max':
|
|
945
1269
|
case 'count':
|
|
1270
|
+
case 'distinct-count':
|
|
946
1271
|
case 'sum':
|
|
947
1272
|
return aggregation;
|
|
948
1273
|
case undefined:
|
|
949
1274
|
return undefined;
|
|
950
1275
|
default:
|
|
951
|
-
throw new Error(`x-ui.chart aggregation "${aggregation}" is not
|
|
1276
|
+
throw new Error(`x-ui.chart aggregation "${aggregation}" is not supported in @praxisui/charts.`);
|
|
952
1277
|
}
|
|
953
1278
|
}
|
|
954
1279
|
toQueryFilterMap(filters) {
|
|
@@ -1072,8 +1397,6 @@ class PraxisChartCanonicalContractMapperService {
|
|
|
1072
1397
|
}
|
|
1073
1398
|
mapStatsGranularityToBackend(value) {
|
|
1074
1399
|
switch (value) {
|
|
1075
|
-
case 'hour':
|
|
1076
|
-
return 'HOUR';
|
|
1077
1400
|
case 'day':
|
|
1078
1401
|
return 'DAY';
|
|
1079
1402
|
case 'week':
|
|
@@ -1081,10 +1404,6 @@ class PraxisChartCanonicalContractMapperService {
|
|
|
1081
1404
|
case 'month':
|
|
1082
1405
|
case undefined:
|
|
1083
1406
|
return 'MONTH';
|
|
1084
|
-
case 'quarter':
|
|
1085
|
-
return 'QUARTER';
|
|
1086
|
-
case 'year':
|
|
1087
|
-
return 'YEAR';
|
|
1088
1407
|
default:
|
|
1089
1408
|
throw new Error(`x-ui.chart source.options.granularity "${value}" is not supported in @praxisui/charts.`);
|
|
1090
1409
|
}
|
|
@@ -1105,6 +1424,8 @@ class PraxisChartCanonicalContractMapperService {
|
|
|
1105
1424
|
case undefined:
|
|
1106
1425
|
case 'count':
|
|
1107
1426
|
return 'COUNT';
|
|
1427
|
+
case 'distinct-count':
|
|
1428
|
+
return 'DISTINCT_COUNT';
|
|
1108
1429
|
case 'sum':
|
|
1109
1430
|
return 'SUM';
|
|
1110
1431
|
case 'avg':
|
|
@@ -1114,7 +1435,7 @@ class PraxisChartCanonicalContractMapperService {
|
|
|
1114
1435
|
case 'max':
|
|
1115
1436
|
return 'MAX';
|
|
1116
1437
|
default:
|
|
1117
|
-
throw new Error(`x-ui.chart aggregation "${aggregation}" is not
|
|
1438
|
+
throw new Error(`x-ui.chart aggregation "${aggregation}" is not supported in @praxisui/charts.`);
|
|
1118
1439
|
}
|
|
1119
1440
|
}
|
|
1120
1441
|
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 });
|
|
@@ -1302,11 +1623,14 @@ class PraxisChartComponent {
|
|
|
1302
1623
|
chartDocument = input(null, ...(ngDevMode ? [{ debugName: "chartDocument" }] : []));
|
|
1303
1624
|
filterCriteria = input(null, ...(ngDevMode ? [{ debugName: "filterCriteria" }] : []));
|
|
1304
1625
|
queryContext = input(null, ...(ngDevMode ? [{ debugName: "queryContext" }] : []));
|
|
1626
|
+
remoteDataResolver = input(null, ...(ngDevMode ? [{ debugName: "remoteDataResolver" }] : []));
|
|
1305
1627
|
enableCustomization = input(false, ...(ngDevMode ? [{ debugName: "enableCustomization", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
|
|
1306
1628
|
availableResources = input([], ...(ngDevMode ? [{ debugName: "availableResources" }] : []));
|
|
1307
1629
|
availableFields = input([], ...(ngDevMode ? [{ debugName: "availableFields" }] : []));
|
|
1308
1630
|
availableTargets = input([], ...(ngDevMode ? [{ debugName: "availableTargets" }] : []));
|
|
1309
1631
|
pointClick = output();
|
|
1632
|
+
selectionChange = output();
|
|
1633
|
+
crossFilter = output();
|
|
1310
1634
|
queryRequest = output();
|
|
1311
1635
|
loadStateChange = output();
|
|
1312
1636
|
chartDocumentApplied = output();
|
|
@@ -1332,6 +1656,7 @@ class PraxisChartComponent {
|
|
|
1332
1656
|
fillContainerHeight = signal(false, ...(ngDevMode ? [{ debugName: "fillContainerHeight" }] : []));
|
|
1333
1657
|
resizeFrameId = null;
|
|
1334
1658
|
previousRemoteSignature = null;
|
|
1659
|
+
previousRemoteDataResolver = undefined;
|
|
1335
1660
|
previousDocumentSignature = null;
|
|
1336
1661
|
editorSessionSubscriptions = [];
|
|
1337
1662
|
effectiveConfig = computed(() => {
|
|
@@ -1363,14 +1688,37 @@ class PraxisChartComponent {
|
|
|
1363
1688
|
return [];
|
|
1364
1689
|
}, ...(ngDevMode ? [{ debugName: "resolvedData" }] : []));
|
|
1365
1690
|
resolvedHeight = computed(() => {
|
|
1366
|
-
|
|
1691
|
+
const sizing = this.effectiveConfig().sizing;
|
|
1692
|
+
const mode = sizing?.mode ?? (sizing?.height !== undefined ? 'fixed' : undefined);
|
|
1693
|
+
if (this.fillContainerHeight() || mode === 'fill-container') {
|
|
1367
1694
|
return '100%';
|
|
1368
1695
|
}
|
|
1696
|
+
if (mode === 'auto') {
|
|
1697
|
+
return 'auto';
|
|
1698
|
+
}
|
|
1699
|
+
const sizingHeight = this.toCssSize(sizing?.height);
|
|
1700
|
+
if (sizingHeight) {
|
|
1701
|
+
return sizingHeight;
|
|
1702
|
+
}
|
|
1369
1703
|
const value = this.effectiveConfig().height;
|
|
1370
1704
|
if (typeof value === 'number')
|
|
1371
1705
|
return `${value}px`;
|
|
1372
1706
|
return value || 'var(--praxis-chart-runtime-height, 320px)';
|
|
1373
1707
|
}, ...(ngDevMode ? [{ debugName: "resolvedHeight" }] : []));
|
|
1708
|
+
resolvedMinHeight = computed(() => {
|
|
1709
|
+
const sizing = this.effectiveConfig().sizing;
|
|
1710
|
+
return this.toCssSize(sizing?.minHeight) ?? (this.isFillContainerMode() ? '0' : null);
|
|
1711
|
+
}, ...(ngDevMode ? [{ debugName: "resolvedMinHeight" }] : []));
|
|
1712
|
+
resolvedMaxHeight = computed(() => this.toCssSize(this.effectiveConfig().sizing?.maxHeight) ?? null, ...(ngDevMode ? [{ debugName: "resolvedMaxHeight" }] : []));
|
|
1713
|
+
resolvedAspectRatio = computed(() => this.toCssAspectRatio(this.effectiveConfig().sizing?.aspectRatio), ...(ngDevMode ? [{ debugName: "resolvedAspectRatio" }] : []));
|
|
1714
|
+
isFillContainerMode = computed(() => {
|
|
1715
|
+
return this.fillContainerHeight() || this.effectiveConfig().sizing?.mode === 'fill-container';
|
|
1716
|
+
}, ...(ngDevMode ? [{ debugName: "isFillContainerMode" }] : []));
|
|
1717
|
+
surfaceMode = computed(() => this.effectiveConfig().theme?.surface?.mode ?? 'auto', ...(ngDevMode ? [{ debugName: "surfaceMode" }] : []));
|
|
1718
|
+
surfaceBackground = computed(() => this.effectiveConfig().theme?.surface?.background ?? null, ...(ngDevMode ? [{ debugName: "surfaceBackground" }] : []));
|
|
1719
|
+
surfaceBorderColor = computed(() => this.effectiveConfig().theme?.surface?.borderColor ?? null, ...(ngDevMode ? [{ debugName: "surfaceBorderColor" }] : []));
|
|
1720
|
+
surfaceBorderWidth = computed(() => this.toCssSize(this.effectiveConfig().theme?.surface?.borderWidth) ?? null, ...(ngDevMode ? [{ debugName: "surfaceBorderWidth" }] : []));
|
|
1721
|
+
surfaceBorderRadius = computed(() => this.toCssSize(this.effectiveConfig().theme?.surface?.borderRadius) ?? null, ...(ngDevMode ? [{ debugName: "surfaceBorderRadius" }] : []));
|
|
1374
1722
|
renderConfig = computed(() => {
|
|
1375
1723
|
const config = this.effectiveConfig();
|
|
1376
1724
|
const explicitData = this.data();
|
|
@@ -1431,7 +1779,7 @@ class PraxisChartComponent {
|
|
|
1431
1779
|
errorDescription = computed(() => this.chartDocumentMappingError()
|
|
1432
1780
|
? this.i18n.resolve({
|
|
1433
1781
|
key: 'praxis.charts.runtime.invalidDocumentDescription',
|
|
1434
|
-
text: 'The canonical chart document could not be mapped to the
|
|
1782
|
+
text: 'The canonical chart document could not be mapped to the Praxis chart runtime. Review the chart contract before continuing.',
|
|
1435
1783
|
})
|
|
1436
1784
|
: this.i18n.resolve(this.renderConfig().state?.error?.description), ...(ngDevMode ? [{ debugName: "errorDescription" }] : []));
|
|
1437
1785
|
editChartLabel = computed(() => this.i18n.resolve({ key: 'praxis.charts.runtime.editChart', text: 'Edit chart settings' }), ...(ngDevMode ? [{ debugName: "editChartLabel" }] : []));
|
|
@@ -1486,13 +1834,16 @@ class PraxisChartComponent {
|
|
|
1486
1834
|
effect(() => {
|
|
1487
1835
|
const config = this.effectiveConfig();
|
|
1488
1836
|
const explicitData = this.data();
|
|
1837
|
+
const remoteDataResolver = this.remoteDataResolver();
|
|
1489
1838
|
const nextSignature = explicitData === null || explicitData === undefined
|
|
1490
1839
|
? this.buildRemoteSignature(config)
|
|
1491
1840
|
: null;
|
|
1492
|
-
if (nextSignature === this.previousRemoteSignature
|
|
1841
|
+
if (nextSignature === this.previousRemoteSignature
|
|
1842
|
+
&& remoteDataResolver === this.previousRemoteDataResolver) {
|
|
1493
1843
|
return;
|
|
1494
1844
|
}
|
|
1495
1845
|
this.previousRemoteSignature = nextSignature;
|
|
1846
|
+
this.previousRemoteDataResolver = remoteDataResolver;
|
|
1496
1847
|
this.remoteResolvedData.set(null);
|
|
1497
1848
|
this.remoteRuntimeState.set('idle');
|
|
1498
1849
|
this.remoteTechnicalError.set(null);
|
|
@@ -1523,7 +1874,7 @@ class PraxisChartComponent {
|
|
|
1523
1874
|
this.remoteRuntimeState.set('loading');
|
|
1524
1875
|
this.remoteResolvedData.set([]);
|
|
1525
1876
|
this.remoteTechnicalError.set(null);
|
|
1526
|
-
this.
|
|
1877
|
+
this.executeRemoteDataRequest(event, effectiveConfig)
|
|
1527
1878
|
.pipe(takeUntilDestroyed(this.destroyRef))
|
|
1528
1879
|
.subscribe({
|
|
1529
1880
|
next: (rows) => {
|
|
@@ -1551,7 +1902,7 @@ class PraxisChartComponent {
|
|
|
1551
1902
|
this.engine.render(host, {
|
|
1552
1903
|
config: this.renderConfig(),
|
|
1553
1904
|
data: this.resolvedData(),
|
|
1554
|
-
onPointClick: (event) => this.
|
|
1905
|
+
onPointClick: (event) => this.handlePointClick(event),
|
|
1555
1906
|
});
|
|
1556
1907
|
this.ensureResizeObserver(host);
|
|
1557
1908
|
});
|
|
@@ -1612,6 +1963,88 @@ class PraxisChartComponent {
|
|
|
1612
1963
|
});
|
|
1613
1964
|
this.editorSessionSubscriptions = [appliedSubscription, savedSubscription];
|
|
1614
1965
|
}
|
|
1966
|
+
handlePointClick(event) {
|
|
1967
|
+
this.pointClick.emit(event);
|
|
1968
|
+
const config = this.renderConfig();
|
|
1969
|
+
const interactions = config.interactions;
|
|
1970
|
+
if (!interactions?.selection && !interactions?.crossFilter) {
|
|
1971
|
+
return;
|
|
1972
|
+
}
|
|
1973
|
+
const selectionEvent = this.buildSelectionEvent(config, event);
|
|
1974
|
+
if (interactions.selection) {
|
|
1975
|
+
this.selectionChange.emit(selectionEvent);
|
|
1976
|
+
}
|
|
1977
|
+
if (interactions.crossFilter) {
|
|
1978
|
+
this.crossFilter.emit({
|
|
1979
|
+
chartId: config.id ?? event.chartId,
|
|
1980
|
+
filters: selectionEvent.filters,
|
|
1981
|
+
target: interactions.eventActions?.crossFilter?.target,
|
|
1982
|
+
action: interactions.eventActions?.crossFilter,
|
|
1983
|
+
source: selectionEvent,
|
|
1984
|
+
});
|
|
1985
|
+
}
|
|
1986
|
+
}
|
|
1987
|
+
buildSelectionEvent(config, event) {
|
|
1988
|
+
const action = config.interactions?.eventActions?.selectionChange;
|
|
1989
|
+
const filters = this.buildEventFilters(config, event, action?.mapping);
|
|
1990
|
+
return {
|
|
1991
|
+
...event,
|
|
1992
|
+
chartId: config.id ?? event.chartId,
|
|
1993
|
+
selected: true,
|
|
1994
|
+
filters,
|
|
1995
|
+
action,
|
|
1996
|
+
};
|
|
1997
|
+
}
|
|
1998
|
+
buildEventFilters(config, event, mapping) {
|
|
1999
|
+
const sourceValues = this.extractPointSourceValues(config, event);
|
|
2000
|
+
if (!mapping || !Object.keys(mapping).length) {
|
|
2001
|
+
return sourceValues;
|
|
2002
|
+
}
|
|
2003
|
+
return Object.entries(mapping).reduce((filters, [sourceField, targetField]) => {
|
|
2004
|
+
if (!targetField) {
|
|
2005
|
+
return filters;
|
|
2006
|
+
}
|
|
2007
|
+
const value = sourceValues[sourceField];
|
|
2008
|
+
if (value !== undefined) {
|
|
2009
|
+
filters[targetField] = value;
|
|
2010
|
+
}
|
|
2011
|
+
return filters;
|
|
2012
|
+
}, {});
|
|
2013
|
+
}
|
|
2014
|
+
extractPointSourceValues(config, event) {
|
|
2015
|
+
const values = {};
|
|
2016
|
+
if (event.data && typeof event.data === 'object' && !Array.isArray(event.data)) {
|
|
2017
|
+
Object.assign(values, event.data);
|
|
2018
|
+
}
|
|
2019
|
+
const categoryField = config.axes?.x?.field
|
|
2020
|
+
?? config.series.find((series) => series.categoryField)?.categoryField;
|
|
2021
|
+
if (categoryField && values[categoryField] === undefined && event.category !== undefined) {
|
|
2022
|
+
values[categoryField] = event.category;
|
|
2023
|
+
}
|
|
2024
|
+
const series = config.series.find((candidate) => candidate.id === event.seriesId)
|
|
2025
|
+
?? config.series.find((candidate) => candidate.name === event.seriesName)
|
|
2026
|
+
?? config.series[0];
|
|
2027
|
+
const metricField = series?.metric?.field;
|
|
2028
|
+
if (metricField && values[metricField] === undefined && event.value !== undefined) {
|
|
2029
|
+
values[metricField] = this.extractPointMetricValue(event.value);
|
|
2030
|
+
}
|
|
2031
|
+
return values;
|
|
2032
|
+
}
|
|
2033
|
+
extractPointMetricValue(value) {
|
|
2034
|
+
return Array.isArray(value) ? value[value.length - 1] : value;
|
|
2035
|
+
}
|
|
2036
|
+
executeRemoteDataRequest(event, config) {
|
|
2037
|
+
const resolver = this.remoteDataResolver();
|
|
2038
|
+
if (!resolver) {
|
|
2039
|
+
return this.statsApi.execute(event, config);
|
|
2040
|
+
}
|
|
2041
|
+
try {
|
|
2042
|
+
return normalizeRemoteDataResolverResult$1(resolver(event, config));
|
|
2043
|
+
}
|
|
2044
|
+
catch (error) {
|
|
2045
|
+
return throwError(() => error);
|
|
2046
|
+
}
|
|
2047
|
+
}
|
|
1615
2048
|
ensureResizeObserver(host) {
|
|
1616
2049
|
if (this.resizeObserver()) {
|
|
1617
2050
|
return;
|
|
@@ -1622,6 +2055,23 @@ class PraxisChartComponent {
|
|
|
1622
2055
|
observer.observe(host);
|
|
1623
2056
|
this.resizeObserver.set(observer);
|
|
1624
2057
|
}
|
|
2058
|
+
toCssSize(value) {
|
|
2059
|
+
if (typeof value === 'number') {
|
|
2060
|
+
return `${value}px`;
|
|
2061
|
+
}
|
|
2062
|
+
const trimmed = value?.trim();
|
|
2063
|
+
if (trimmed && /^-?\d+(\.\d+)?$/.test(trimmed)) {
|
|
2064
|
+
return `${trimmed}px`;
|
|
2065
|
+
}
|
|
2066
|
+
return trimmed || null;
|
|
2067
|
+
}
|
|
2068
|
+
toCssAspectRatio(value) {
|
|
2069
|
+
if (typeof value === 'number') {
|
|
2070
|
+
return Number.isFinite(value) && value > 0 ? String(value) : null;
|
|
2071
|
+
}
|
|
2072
|
+
const trimmed = value?.trim();
|
|
2073
|
+
return trimmed || null;
|
|
2074
|
+
}
|
|
1625
2075
|
buildRemoteSignature(config) {
|
|
1626
2076
|
if (config.dataSource?.kind !== 'remote') {
|
|
1627
2077
|
return null;
|
|
@@ -1646,7 +2096,7 @@ class PraxisChartComponent {
|
|
|
1646
2096
|
return;
|
|
1647
2097
|
}
|
|
1648
2098
|
const syncShellMode = () => {
|
|
1649
|
-
this.fillContainerHeight.set(
|
|
2099
|
+
this.fillContainerHeight.set(this.shouldFillShellContainer(shell));
|
|
1650
2100
|
this.scheduleResizeAfterShellModeChange();
|
|
1651
2101
|
};
|
|
1652
2102
|
syncShellMode();
|
|
@@ -1659,6 +2109,12 @@ class PraxisChartComponent {
|
|
|
1659
2109
|
});
|
|
1660
2110
|
this.shellObserver.set(observer);
|
|
1661
2111
|
}
|
|
2112
|
+
shouldFillShellContainer(shell) {
|
|
2113
|
+
return (shell.classList.contains('no-shell') ||
|
|
2114
|
+
shell.classList.contains('body-fill') ||
|
|
2115
|
+
shell.classList.contains('expanded') ||
|
|
2116
|
+
shell.classList.contains('fullscreen'));
|
|
2117
|
+
}
|
|
1662
2118
|
scheduleResizeAfterShellModeChange() {
|
|
1663
2119
|
this.scheduleEngineResize();
|
|
1664
2120
|
}
|
|
@@ -1685,14 +2141,27 @@ class PraxisChartComponent {
|
|
|
1685
2141
|
this.resizeFrameId = null;
|
|
1686
2142
|
}
|
|
1687
2143
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: PraxisChartComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1688
|
-
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: [
|
|
2144
|
+
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 }, remoteDataResolver: { classPropertyName: "remoteDataResolver", publicName: "remoteDataResolver", isSignal: true, isRequired: false, transformFunction: null }, enableCustomization: { classPropertyName: "enableCustomization", publicName: "enableCustomization", isSignal: true, isRequired: false, transformFunction: null }, availableResources: { classPropertyName: "availableResources", publicName: "availableResources", isSignal: true, isRequired: false, transformFunction: null }, availableFields: { classPropertyName: "availableFields", publicName: "availableFields", isSignal: true, isRequired: false, transformFunction: null }, availableTargets: { classPropertyName: "availableTargets", publicName: "availableTargets", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { pointClick: "pointClick", selectionChange: "selectionChange", crossFilter: "crossFilter", queryRequest: "queryRequest", loadStateChange: "loadStateChange", chartDocumentApplied: "chartDocumentApplied", chartDocumentSaved: "chartDocumentSaved" }, providers: [
|
|
1689
2145
|
EChartsEngineAdapter,
|
|
1690
2146
|
{
|
|
1691
2147
|
provide: PRAXIS_CHART_ENGINE,
|
|
1692
2148
|
useExisting: EChartsEngineAdapter,
|
|
1693
2149
|
},
|
|
1694
2150
|
], viewQueries: [{ propertyName: "chartHost", first: true, predicate: ["chartHost"], descendants: true, isSignal: true }], ngImport: i0, template: `
|
|
1695
|
-
<section
|
|
2151
|
+
<section
|
|
2152
|
+
class="praxis-chart-shell"
|
|
2153
|
+
[class.praxis-chart-shell-fill-container]="isFillContainerMode()"
|
|
2154
|
+
[class.praxis-chart-shell-contained]="surfaceMode() === 'contained'"
|
|
2155
|
+
[class.praxis-chart-shell-embedded]="surfaceMode() === 'embedded'"
|
|
2156
|
+
[style.height]="resolvedHeight()"
|
|
2157
|
+
[style.minHeight]="resolvedMinHeight()"
|
|
2158
|
+
[style.maxHeight]="resolvedMaxHeight()"
|
|
2159
|
+
[style.aspectRatio]="resolvedAspectRatio()"
|
|
2160
|
+
[style.--praxis-chart-config-surface-bg]="surfaceBackground()"
|
|
2161
|
+
[style.--praxis-chart-config-surface-border]="surfaceBorderColor()"
|
|
2162
|
+
[style.--praxis-chart-config-surface-border-width]="surfaceBorderWidth()"
|
|
2163
|
+
[style.--praxis-chart-config-surface-radius]="surfaceBorderRadius()"
|
|
2164
|
+
>
|
|
1696
2165
|
@if (canOpenConfigEditor()) {
|
|
1697
2166
|
<button
|
|
1698
2167
|
class="praxis-chart-settings-trigger"
|
|
@@ -1752,7 +2221,7 @@ class PraxisChartComponent {
|
|
|
1752
2221
|
<div #chartHost class="praxis-chart-host"></div>
|
|
1753
2222
|
}
|
|
1754
2223
|
</section>
|
|
1755
|
-
`, 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:
|
|
2224
|
+
`, isInline: true, styles: [":host{display:block;height:100%;min-width:0}:host-context(.pdx-shell.no-shell) .praxis-chart-shell,:host-context(.pdx-shell.body-fill) .praxis-chart-shell,:host-context(.pdx-shell.expanded) .praxis-chart-shell,:host-context(.pdx-shell.fullscreen) .praxis-chart-shell{height:100%!important}.praxis-chart-shell{position:relative;width:100%;height:100%;min-height:240px;border-radius:var(--praxis-chart-config-surface-radius, var(--praxis-chart-surface-radius, 8px));overflow:hidden;background:var( --praxis-chart-config-surface-bg, var(--praxis-chart-surface-bg, var(--md-sys-color-surface-container-lowest, #fff)) );border-color:var( --praxis-chart-config-surface-border, var( --praxis-chart-surface-border, color-mix(in srgb, var(--md-sys-color-outline, #c5c7ce) 44%, transparent) ) );border-style:solid;border-width:var(--praxis-chart-config-surface-border-width, 1px)}.praxis-chart-shell-fill-container{min-height:0}:host-context(.pdx-shell) .praxis-chart-shell:not(.praxis-chart-shell-contained),.praxis-chart-shell-embedded{border-width:var( --praxis-chart-config-surface-border-width, var(--praxis-chart-embedded-surface-border-width, 0) );border-radius:var( --praxis-chart-config-surface-radius, var(--praxis-chart-embedded-surface-radius, 0) );background:var( --praxis-chart-config-surface-bg, var(--praxis-chart-embedded-surface-bg, transparent) )}.praxis-chart-settings-trigger{position:absolute;top:10px;right:10px;z-index:3;background:color-mix(in srgb,var(--md-sys-color-surface, #fff) 88%,rgba(18,99,180,.12));-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px)}.praxis-chart-host{width:100%;height:100%}.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 });
|
|
1756
2225
|
}
|
|
1757
2226
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: PraxisChartComponent, decorators: [{
|
|
1758
2227
|
type: Component,
|
|
@@ -1763,7 +2232,20 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
|
|
|
1763
2232
|
useExisting: EChartsEngineAdapter,
|
|
1764
2233
|
},
|
|
1765
2234
|
], changeDetection: ChangeDetectionStrategy.OnPush, template: `
|
|
1766
|
-
<section
|
|
2235
|
+
<section
|
|
2236
|
+
class="praxis-chart-shell"
|
|
2237
|
+
[class.praxis-chart-shell-fill-container]="isFillContainerMode()"
|
|
2238
|
+
[class.praxis-chart-shell-contained]="surfaceMode() === 'contained'"
|
|
2239
|
+
[class.praxis-chart-shell-embedded]="surfaceMode() === 'embedded'"
|
|
2240
|
+
[style.height]="resolvedHeight()"
|
|
2241
|
+
[style.minHeight]="resolvedMinHeight()"
|
|
2242
|
+
[style.maxHeight]="resolvedMaxHeight()"
|
|
2243
|
+
[style.aspectRatio]="resolvedAspectRatio()"
|
|
2244
|
+
[style.--praxis-chart-config-surface-bg]="surfaceBackground()"
|
|
2245
|
+
[style.--praxis-chart-config-surface-border]="surfaceBorderColor()"
|
|
2246
|
+
[style.--praxis-chart-config-surface-border-width]="surfaceBorderWidth()"
|
|
2247
|
+
[style.--praxis-chart-config-surface-radius]="surfaceBorderRadius()"
|
|
2248
|
+
>
|
|
1767
2249
|
@if (canOpenConfigEditor()) {
|
|
1768
2250
|
<button
|
|
1769
2251
|
class="praxis-chart-settings-trigger"
|
|
@@ -1823,8 +2305,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
|
|
|
1823
2305
|
<div #chartHost class="praxis-chart-host"></div>
|
|
1824
2306
|
}
|
|
1825
2307
|
</section>
|
|
1826
|
-
`, 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:
|
|
1827
|
-
}], 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 }] }] } });
|
|
2308
|
+
`, styles: [":host{display:block;height:100%;min-width:0}:host-context(.pdx-shell.no-shell) .praxis-chart-shell,:host-context(.pdx-shell.body-fill) .praxis-chart-shell,:host-context(.pdx-shell.expanded) .praxis-chart-shell,:host-context(.pdx-shell.fullscreen) .praxis-chart-shell{height:100%!important}.praxis-chart-shell{position:relative;width:100%;height:100%;min-height:240px;border-radius:var(--praxis-chart-config-surface-radius, var(--praxis-chart-surface-radius, 8px));overflow:hidden;background:var( --praxis-chart-config-surface-bg, var(--praxis-chart-surface-bg, var(--md-sys-color-surface-container-lowest, #fff)) );border-color:var( --praxis-chart-config-surface-border, var( --praxis-chart-surface-border, color-mix(in srgb, var(--md-sys-color-outline, #c5c7ce) 44%, transparent) ) );border-style:solid;border-width:var(--praxis-chart-config-surface-border-width, 1px)}.praxis-chart-shell-fill-container{min-height:0}:host-context(.pdx-shell) .praxis-chart-shell:not(.praxis-chart-shell-contained),.praxis-chart-shell-embedded{border-width:var( --praxis-chart-config-surface-border-width, var(--praxis-chart-embedded-surface-border-width, 0) );border-radius:var( --praxis-chart-config-surface-radius, var(--praxis-chart-embedded-surface-radius, 0) );background:var( --praxis-chart-config-surface-bg, var(--praxis-chart-embedded-surface-bg, transparent) )}.praxis-chart-settings-trigger{position:absolute;top:10px;right:10px;z-index:3;background:color-mix(in srgb,var(--md-sys-color-surface, #fff) 88%,rgba(18,99,180,.12));-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px)}.praxis-chart-host{width:100%;height:100%}.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"] }]
|
|
2309
|
+
}], 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 }] }], remoteDataResolver: [{ type: i0.Input, args: [{ isSignal: true, alias: "remoteDataResolver", required: false }] }], enableCustomization: [{ type: i0.Input, args: [{ isSignal: true, alias: "enableCustomization", required: false }] }], availableResources: [{ type: i0.Input, args: [{ isSignal: true, alias: "availableResources", required: false }] }], availableFields: [{ type: i0.Input, args: [{ isSignal: true, alias: "availableFields", required: false }] }], availableTargets: [{ type: i0.Input, args: [{ isSignal: true, alias: "availableTargets", required: false }] }], pointClick: [{ type: i0.Output, args: ["pointClick"] }], selectionChange: [{ type: i0.Output, args: ["selectionChange"] }], crossFilter: [{ type: i0.Output, args: ["crossFilter"] }], queryRequest: [{ type: i0.Output, args: ["queryRequest"] }], loadStateChange: [{ type: i0.Output, args: ["loadStateChange"] }], chartDocumentApplied: [{ type: i0.Output, args: ["chartDocumentApplied"] }], chartDocumentSaved: [{ type: i0.Output, args: ["chartDocumentSaved"] }], chartHost: [{ type: i0.ViewChild, args: ['chartHost', { isSignal: true }] }] } });
|
|
1828
2310
|
function normalizeFilterCriteria(criteria) {
|
|
1829
2311
|
if (!criteria)
|
|
1830
2312
|
return null;
|
|
@@ -1914,6 +2396,18 @@ function mergeRemoteQueryContext(config, queryContext, runtimeFilters) {
|
|
|
1914
2396
|
},
|
|
1915
2397
|
};
|
|
1916
2398
|
}
|
|
2399
|
+
function normalizeRemoteDataResolverResult$1(result) {
|
|
2400
|
+
if (Array.isArray(result)) {
|
|
2401
|
+
return of(result);
|
|
2402
|
+
}
|
|
2403
|
+
if (isObservable(result)) {
|
|
2404
|
+
return result;
|
|
2405
|
+
}
|
|
2406
|
+
if (result && typeof result.then === 'function') {
|
|
2407
|
+
return from(result);
|
|
2408
|
+
}
|
|
2409
|
+
return of([]);
|
|
2410
|
+
}
|
|
1917
2411
|
|
|
1918
2412
|
const PRAXIS_CHART_DRILLDOWN_DATA_BY_MONTH = {
|
|
1919
2413
|
Jan: [
|
|
@@ -1994,7 +2488,7 @@ class PraxisChartDrilldownPanelComponent {
|
|
|
1994
2488
|
[data]="detailData()"
|
|
1995
2489
|
></praxis-chart>
|
|
1996
2490
|
</section>
|
|
1997
|
-
`, isInline: true, styles: [":host{display:block}.drilldown-shell{display:grid;gap:14px}.drilldown-header h3,.drilldown-eyebrow,.drilldown-description{margin:0}.drilldown-eyebrow{font-size:.75rem;letter-spacing:.14em;text-transform:uppercase;color:var(--md-sys-color-on-surface-variant, #5a5d67)}.drilldown-header h3{font-size:1.2rem;color:var(--md-sys-color-on-surface, #1a1b20)}.drilldown-description{color:var(--md-sys-color-on-surface-variant, #5a5d67)}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { 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 });
|
|
2491
|
+
`, isInline: true, styles: [":host{display:block}.drilldown-shell{display:grid;gap:14px}.drilldown-header h3,.drilldown-eyebrow,.drilldown-description{margin:0}.drilldown-eyebrow{font-size:.75rem;letter-spacing:.14em;text-transform:uppercase;color:var(--md-sys-color-on-surface-variant, #5a5d67)}.drilldown-header h3{font-size:1.2rem;color:var(--md-sys-color-on-surface, #1a1b20)}.drilldown-description{color:var(--md-sys-color-on-surface-variant, #5a5d67)}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: PraxisChartComponent, selector: "praxis-chart", inputs: ["config", "data", "chartDocument", "filterCriteria", "queryContext", "remoteDataResolver", "enableCustomization", "availableResources", "availableFields", "availableTargets"], outputs: ["pointClick", "selectionChange", "crossFilter", "queryRequest", "loadStateChange", "chartDocumentApplied", "chartDocumentSaved"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
1998
2492
|
}
|
|
1999
2493
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: PraxisChartDrilldownPanelComponent, decorators: [{
|
|
2000
2494
|
type: Component,
|
|
@@ -2087,6 +2581,19 @@ const PRAXIS_CHART_PHASE1_PORTS = [
|
|
|
2087
2581
|
},
|
|
2088
2582
|
exposure: { public: true, group: 'config' },
|
|
2089
2583
|
},
|
|
2584
|
+
{
|
|
2585
|
+
id: 'remoteDataResolver',
|
|
2586
|
+
label: 'Remote Data Resolver',
|
|
2587
|
+
direction: 'input',
|
|
2588
|
+
semanticKind: 'collection',
|
|
2589
|
+
description: 'Optional host resolver that owns remote chart data execution for governed integrations outside the default praxis.stats client.',
|
|
2590
|
+
schema: {
|
|
2591
|
+
id: 'PraxisChartRemoteDataResolver',
|
|
2592
|
+
kind: 'ts-type',
|
|
2593
|
+
ref: 'PraxisChartRemoteDataResolver',
|
|
2594
|
+
},
|
|
2595
|
+
exposure: { public: true, group: 'data' },
|
|
2596
|
+
},
|
|
2090
2597
|
{
|
|
2091
2598
|
id: 'config',
|
|
2092
2599
|
label: 'Chart Config',
|
|
@@ -2114,6 +2621,34 @@ const PRAXIS_CHART_PHASE1_PORTS = [
|
|
|
2114
2621
|
},
|
|
2115
2622
|
exposure: { public: true, group: 'interaction' },
|
|
2116
2623
|
},
|
|
2624
|
+
{
|
|
2625
|
+
id: 'selectionChange',
|
|
2626
|
+
label: 'Selection Change',
|
|
2627
|
+
direction: 'output',
|
|
2628
|
+
semanticKind: 'event',
|
|
2629
|
+
description: 'Canonical selected point payload emitted when declarative chart selection is enabled.',
|
|
2630
|
+
cardinality: 'stream',
|
|
2631
|
+
schema: {
|
|
2632
|
+
id: 'PraxisChartSelectionEvent',
|
|
2633
|
+
kind: 'ts-type',
|
|
2634
|
+
ref: 'PraxisChartSelectionEvent',
|
|
2635
|
+
},
|
|
2636
|
+
exposure: { public: true, group: 'interaction' },
|
|
2637
|
+
},
|
|
2638
|
+
{
|
|
2639
|
+
id: 'crossFilter',
|
|
2640
|
+
label: 'Cross Filter',
|
|
2641
|
+
direction: 'output',
|
|
2642
|
+
semanticKind: 'query-context',
|
|
2643
|
+
description: 'Canonical filter payload derived from selected chart points for connected hosts.',
|
|
2644
|
+
cardinality: 'stream',
|
|
2645
|
+
schema: {
|
|
2646
|
+
id: 'PraxisChartCrossFilterEvent',
|
|
2647
|
+
kind: 'ts-type',
|
|
2648
|
+
ref: 'PraxisChartCrossFilterEvent',
|
|
2649
|
+
},
|
|
2650
|
+
exposure: { public: true, group: 'interaction' },
|
|
2651
|
+
},
|
|
2117
2652
|
{
|
|
2118
2653
|
id: 'queryRequest',
|
|
2119
2654
|
label: 'Query Request',
|
|
@@ -2175,6 +2710,11 @@ const PRAXIS_CHART_COMPONENT_METADATA = {
|
|
|
2175
2710
|
type: 'PraxisDataQueryContext | null',
|
|
2176
2711
|
description: 'Primary semantic query contract for dynamic-page orchestration. Charts consume filters, sort and limit from queryContext while preserving filterCriteria as a compatibility bridge.',
|
|
2177
2712
|
},
|
|
2713
|
+
{
|
|
2714
|
+
name: 'remoteDataResolver',
|
|
2715
|
+
type: 'PraxisChartRemoteDataResolver | null',
|
|
2716
|
+
description: 'Optional host resolver for remote chart rows. When provided, the runtime emits queryRequest and delegates row resolution to the resolver instead of the default praxis.stats client.',
|
|
2717
|
+
},
|
|
2178
2718
|
{
|
|
2179
2719
|
name: 'filterCriteria',
|
|
2180
2720
|
type: 'Record<string, unknown> | null',
|
|
@@ -2211,6 +2751,16 @@ const PRAXIS_CHART_COMPONENT_METADATA = {
|
|
|
2211
2751
|
type: 'PraxisChartPointEvent',
|
|
2212
2752
|
description: 'Emitted when the host wants to react to a point/series click.',
|
|
2213
2753
|
},
|
|
2754
|
+
{
|
|
2755
|
+
name: 'selectionChange',
|
|
2756
|
+
type: 'PraxisChartSelectionEvent',
|
|
2757
|
+
description: 'Emitted when declarative chart selection resolves a selected point and its canonical filters.',
|
|
2758
|
+
},
|
|
2759
|
+
{
|
|
2760
|
+
name: 'crossFilter',
|
|
2761
|
+
type: 'PraxisChartCrossFilterEvent',
|
|
2762
|
+
description: 'Emitted when declarative cross-filtering resolves filters from a selected chart point.',
|
|
2763
|
+
},
|
|
2214
2764
|
{
|
|
2215
2765
|
name: 'queryRequest',
|
|
2216
2766
|
type: 'PraxisChartQueryRequestEvent',
|
|
@@ -2329,9 +2879,12 @@ class PraxisChartShowcaseWidgetComponent {
|
|
|
2329
2879
|
config = input.required(...(ngDevMode ? [{ debugName: "config" }] : []));
|
|
2330
2880
|
data = input(null, ...(ngDevMode ? [{ debugName: "data" }] : []));
|
|
2331
2881
|
chartDocument = input(null, ...(ngDevMode ? [{ debugName: "chartDocument" }] : []));
|
|
2882
|
+
remoteDataResolver = input(null, ...(ngDevMode ? [{ debugName: "remoteDataResolver" }] : []));
|
|
2332
2883
|
enableCustomization = input(false, ...(ngDevMode ? [{ debugName: "enableCustomization" }] : []));
|
|
2333
2884
|
viewMode = input('chart', ...(ngDevMode ? [{ debugName: "viewMode" }] : []));
|
|
2334
2885
|
pointClick = output();
|
|
2886
|
+
selectionChange = output();
|
|
2887
|
+
crossFilter = output();
|
|
2335
2888
|
queryRequest = output();
|
|
2336
2889
|
loadStateChange = output();
|
|
2337
2890
|
statsApi = inject(PraxisChartStatsApiService);
|
|
@@ -2340,6 +2893,7 @@ class PraxisChartShowcaseWidgetComponent {
|
|
|
2340
2893
|
remoteRuntimeState = signal('idle', ...(ngDevMode ? [{ debugName: "remoteRuntimeState" }] : []));
|
|
2341
2894
|
remoteTechnicalError = signal(null, ...(ngDevMode ? [{ debugName: "remoteTechnicalError" }] : []));
|
|
2342
2895
|
previousRemoteSignature = null;
|
|
2896
|
+
previousRemoteDataResolver = undefined;
|
|
2343
2897
|
resolvedRows = computed(() => {
|
|
2344
2898
|
const explicitData = this.data();
|
|
2345
2899
|
if (explicitData !== null && explicitData !== undefined) {
|
|
@@ -2361,13 +2915,16 @@ class PraxisChartShowcaseWidgetComponent {
|
|
|
2361
2915
|
effect(() => {
|
|
2362
2916
|
const config = this.config();
|
|
2363
2917
|
const explicitData = this.data();
|
|
2918
|
+
const remoteDataResolver = this.remoteDataResolver();
|
|
2364
2919
|
const nextSignature = explicitData === null || explicitData === undefined
|
|
2365
2920
|
? buildRemoteSignature(config)
|
|
2366
2921
|
: null;
|
|
2367
|
-
if (nextSignature === this.previousRemoteSignature
|
|
2922
|
+
if (nextSignature === this.previousRemoteSignature
|
|
2923
|
+
&& remoteDataResolver === this.previousRemoteDataResolver) {
|
|
2368
2924
|
return;
|
|
2369
2925
|
}
|
|
2370
2926
|
this.previousRemoteSignature = nextSignature;
|
|
2927
|
+
this.previousRemoteDataResolver = remoteDataResolver;
|
|
2371
2928
|
this.remoteResolvedRows.set(null);
|
|
2372
2929
|
this.remoteRuntimeState.set('idle');
|
|
2373
2930
|
this.remoteTechnicalError.set(null);
|
|
@@ -2393,7 +2950,7 @@ class PraxisChartShowcaseWidgetComponent {
|
|
|
2393
2950
|
this.remoteRuntimeState.set('loading');
|
|
2394
2951
|
this.remoteResolvedRows.set([]);
|
|
2395
2952
|
this.remoteTechnicalError.set(null);
|
|
2396
|
-
this.
|
|
2953
|
+
this.executeRemoteDataRequest(event, config)
|
|
2397
2954
|
.pipe(takeUntilDestroyed(this.destroyRef))
|
|
2398
2955
|
.subscribe({
|
|
2399
2956
|
next: (rows) => {
|
|
@@ -2411,6 +2968,18 @@ class PraxisChartShowcaseWidgetComponent {
|
|
|
2411
2968
|
});
|
|
2412
2969
|
});
|
|
2413
2970
|
}
|
|
2971
|
+
executeRemoteDataRequest(event, config) {
|
|
2972
|
+
const resolver = this.remoteDataResolver();
|
|
2973
|
+
if (!resolver) {
|
|
2974
|
+
return this.statsApi.execute(event, config);
|
|
2975
|
+
}
|
|
2976
|
+
try {
|
|
2977
|
+
return normalizeRemoteDataResolverResult(resolver(event, config));
|
|
2978
|
+
}
|
|
2979
|
+
catch (error) {
|
|
2980
|
+
return throwError(() => error);
|
|
2981
|
+
}
|
|
2982
|
+
}
|
|
2414
2983
|
handleRowClick(event) {
|
|
2415
2984
|
const row = event?.row;
|
|
2416
2985
|
if (!row) {
|
|
@@ -2430,14 +2999,17 @@ class PraxisChartShowcaseWidgetComponent {
|
|
|
2430
2999
|
});
|
|
2431
3000
|
}
|
|
2432
3001
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: PraxisChartShowcaseWidgetComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
2433
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.17", type: PraxisChartShowcaseWidgetComponent, isStandalone: true, selector: "praxis-chart-showcase-widget", inputs: { config: { classPropertyName: "config", publicName: "config", isSignal: true, isRequired: true, transformFunction: null }, data: { classPropertyName: "data", publicName: "data", isSignal: true, isRequired: false, transformFunction: null }, chartDocument: { classPropertyName: "chartDocument", publicName: "chartDocument", isSignal: true, isRequired: false, transformFunction: null }, enableCustomization: { classPropertyName: "enableCustomization", publicName: "enableCustomization", isSignal: true, isRequired: false, transformFunction: null }, viewMode: { classPropertyName: "viewMode", publicName: "viewMode", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { pointClick: "pointClick", queryRequest: "queryRequest", loadStateChange: "loadStateChange" }, ngImport: i0, template: `
|
|
3002
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.17", type: PraxisChartShowcaseWidgetComponent, isStandalone: true, selector: "praxis-chart-showcase-widget", inputs: { config: { classPropertyName: "config", publicName: "config", isSignal: true, isRequired: true, transformFunction: null }, data: { classPropertyName: "data", publicName: "data", isSignal: true, isRequired: false, transformFunction: null }, chartDocument: { classPropertyName: "chartDocument", publicName: "chartDocument", isSignal: true, isRequired: false, transformFunction: null }, remoteDataResolver: { classPropertyName: "remoteDataResolver", publicName: "remoteDataResolver", isSignal: true, isRequired: false, transformFunction: null }, enableCustomization: { classPropertyName: "enableCustomization", publicName: "enableCustomization", isSignal: true, isRequired: false, transformFunction: null }, viewMode: { classPropertyName: "viewMode", publicName: "viewMode", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { pointClick: "pointClick", selectionChange: "selectionChange", crossFilter: "crossFilter", queryRequest: "queryRequest", loadStateChange: "loadStateChange" }, ngImport: i0, template: `
|
|
2434
3003
|
@if (viewMode() === 'chart') {
|
|
2435
3004
|
<praxis-chart
|
|
2436
3005
|
[config]="config()"
|
|
2437
3006
|
[data]="data()"
|
|
2438
3007
|
[chartDocument]="chartDocument()"
|
|
3008
|
+
[remoteDataResolver]="remoteDataResolver()"
|
|
2439
3009
|
[enableCustomization]="enableCustomization()"
|
|
2440
3010
|
(pointClick)="pointClick.emit($event)"
|
|
3011
|
+
(selectionChange)="selectionChange.emit($event)"
|
|
3012
|
+
(crossFilter)="crossFilter.emit($event)"
|
|
2441
3013
|
(queryRequest)="queryRequest.emit($event)"
|
|
2442
3014
|
(loadStateChange)="loadStateChange.emit($event)"
|
|
2443
3015
|
></praxis-chart>
|
|
@@ -2463,7 +3035,7 @@ class PraxisChartShowcaseWidgetComponent {
|
|
|
2463
3035
|
(rowClick)="handleRowClick($event)"
|
|
2464
3036
|
></praxis-table>
|
|
2465
3037
|
}
|
|
2466
|
-
`, isInline: true, styles: [":host{display:block;height:100%;min-width:0}.showcase-state-card{min-height:240px;display:grid;place-content:center;gap:8px;padding:24px;border-radius:18px;border:1px solid color-mix(in srgb,var(--md-sys-color-outline, #c5c7ce) 72%,transparent);background:linear-gradient(180deg,#fffffff5,#f4f7fbfa);text-align:center}.showcase-state-card h4{margin:0;font-size:1rem;font-weight:600;color:var(--md-sys-color-on-surface, #1a1b20)}.showcase-state-card p{margin:0;color:var(--md-sys-color-on-surface-variant, #5a5d67)}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: PraxisChartComponent, selector: "praxis-chart", inputs: ["config", "data", "chartDocument", "filterCriteria", "queryContext", "enableCustomization", "availableResources", "availableFields", "availableTargets"], outputs: ["pointClick", "queryRequest", "loadStateChange", "chartDocumentApplied", "chartDocumentSaved"] }, { kind: "component", type: PraxisTable, selector: "praxis-table", inputs: ["config", "resourcePath", "data", "tableId", "componentInstanceId", "title", "subtitle", "icon", "autoDelete", "notifyIfOutdated", "snoozeMs", "autoOpenSettingsOnOutdated", "crudContext", "dslFunctionRegistry", "filterCriteria", "queryContext", "enableCustomization", "dense"], outputs: ["rowClick", "rowDoubleClick", "rowExpansionChange", "rowAction", "toolbarAction", "bulkAction", "columnReorder", "columnReorderAttempt", "beforeDelete", "afterDelete", "deleteError", "beforeBulkDelete", "afterBulkDelete", "bulkDeleteError", "schemaStatusChange", "metadataChange", "loadingStateChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
3038
|
+
`, isInline: true, styles: [":host{display:block;height:100%;min-width:0}.showcase-state-card{min-height:240px;display:grid;place-content:center;gap:8px;padding:24px;border-radius:18px;border:1px solid color-mix(in srgb,var(--md-sys-color-outline, #c5c7ce) 72%,transparent);background:linear-gradient(180deg,#fffffff5,#f4f7fbfa);text-align:center}.showcase-state-card h4{margin:0;font-size:1rem;font-weight:600;color:var(--md-sys-color-on-surface, #1a1b20)}.showcase-state-card p{margin:0;color:var(--md-sys-color-on-surface-variant, #5a5d67)}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: PraxisChartComponent, selector: "praxis-chart", inputs: ["config", "data", "chartDocument", "filterCriteria", "queryContext", "remoteDataResolver", "enableCustomization", "availableResources", "availableFields", "availableTargets"], outputs: ["pointClick", "selectionChange", "crossFilter", "queryRequest", "loadStateChange", "chartDocumentApplied", "chartDocumentSaved"] }, { kind: "component", type: PraxisTable, selector: "praxis-table", inputs: ["config", "resourcePath", "data", "tableId", "componentInstanceId", "title", "subtitle", "icon", "autoDelete", "notifyIfOutdated", "snoozeMs", "autoOpenSettingsOnOutdated", "crudContext", "dslFunctionRegistry", "filterCriteria", "queryContext", "enableCustomization", "dense"], outputs: ["rowClick", "rowDoubleClick", "rowExpansionChange", "rowAction", "toolbarAction", "bulkAction", "columnReorder", "columnReorderAttempt", "beforeDelete", "afterDelete", "deleteError", "beforeBulkDelete", "afterBulkDelete", "bulkDeleteError", "schemaStatusChange", "metadataChange", "loadingStateChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
2467
3039
|
}
|
|
2468
3040
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: PraxisChartShowcaseWidgetComponent, decorators: [{
|
|
2469
3041
|
type: Component,
|
|
@@ -2473,8 +3045,11 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
|
|
|
2473
3045
|
[config]="config()"
|
|
2474
3046
|
[data]="data()"
|
|
2475
3047
|
[chartDocument]="chartDocument()"
|
|
3048
|
+
[remoteDataResolver]="remoteDataResolver()"
|
|
2476
3049
|
[enableCustomization]="enableCustomization()"
|
|
2477
3050
|
(pointClick)="pointClick.emit($event)"
|
|
3051
|
+
(selectionChange)="selectionChange.emit($event)"
|
|
3052
|
+
(crossFilter)="crossFilter.emit($event)"
|
|
2478
3053
|
(queryRequest)="queryRequest.emit($event)"
|
|
2479
3054
|
(loadStateChange)="loadStateChange.emit($event)"
|
|
2480
3055
|
></praxis-chart>
|
|
@@ -2501,7 +3076,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
|
|
|
2501
3076
|
></praxis-table>
|
|
2502
3077
|
}
|
|
2503
3078
|
`, styles: [":host{display:block;height:100%;min-width:0}.showcase-state-card{min-height:240px;display:grid;place-content:center;gap:8px;padding:24px;border-radius:18px;border:1px solid color-mix(in srgb,var(--md-sys-color-outline, #c5c7ce) 72%,transparent);background:linear-gradient(180deg,#fffffff5,#f4f7fbfa);text-align:center}.showcase-state-card h4{margin:0;font-size:1rem;font-weight:600;color:var(--md-sys-color-on-surface, #1a1b20)}.showcase-state-card p{margin:0;color:var(--md-sys-color-on-surface-variant, #5a5d67)}\n"] }]
|
|
2504
|
-
}], 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 }] }], enableCustomization: [{ type: i0.Input, args: [{ isSignal: true, alias: "enableCustomization", required: false }] }], viewMode: [{ type: i0.Input, args: [{ isSignal: true, alias: "viewMode", required: false }] }], pointClick: [{ type: i0.Output, args: ["pointClick"] }], queryRequest: [{ type: i0.Output, args: ["queryRequest"] }], loadStateChange: [{ type: i0.Output, args: ["loadStateChange"] }] } });
|
|
3079
|
+
}], ctorParameters: () => [], propDecorators: { config: [{ type: i0.Input, args: [{ isSignal: true, alias: "config", required: true }] }], data: [{ type: i0.Input, args: [{ isSignal: true, alias: "data", required: false }] }], chartDocument: [{ type: i0.Input, args: [{ isSignal: true, alias: "chartDocument", required: false }] }], remoteDataResolver: [{ type: i0.Input, args: [{ isSignal: true, alias: "remoteDataResolver", required: false }] }], enableCustomization: [{ type: i0.Input, args: [{ isSignal: true, alias: "enableCustomization", required: false }] }], viewMode: [{ type: i0.Input, args: [{ isSignal: true, alias: "viewMode", required: false }] }], pointClick: [{ type: i0.Output, args: ["pointClick"] }], selectionChange: [{ type: i0.Output, args: ["selectionChange"] }], crossFilter: [{ type: i0.Output, args: ["crossFilter"] }], queryRequest: [{ type: i0.Output, args: ["queryRequest"] }], loadStateChange: [{ type: i0.Output, args: ["loadStateChange"] }] } });
|
|
2505
3080
|
function resolveShowcaseStateMode(config, remoteRuntimeState = 'idle') {
|
|
2506
3081
|
if (config.preferredLoadState === 'loading') {
|
|
2507
3082
|
return 'loading';
|
|
@@ -2596,6 +3171,18 @@ function buildRemoteSignature(config) {
|
|
|
2596
3171
|
query: config.dataSource.query,
|
|
2597
3172
|
});
|
|
2598
3173
|
}
|
|
3174
|
+
function normalizeRemoteDataResolverResult(result) {
|
|
3175
|
+
if (Array.isArray(result)) {
|
|
3176
|
+
return of(result);
|
|
3177
|
+
}
|
|
3178
|
+
if (isObservable(result)) {
|
|
3179
|
+
return result;
|
|
3180
|
+
}
|
|
3181
|
+
if (result && typeof result.then === 'function') {
|
|
3182
|
+
return from(result);
|
|
3183
|
+
}
|
|
3184
|
+
return of([]);
|
|
3185
|
+
}
|
|
2599
3186
|
|
|
2600
3187
|
const PRAXIS_CHART_SHOWCASE_WIDGET_METADATA = {
|
|
2601
3188
|
id: 'praxis-chart-showcase-widget',
|
|
@@ -2641,6 +3228,16 @@ const PRAXIS_CHART_SHOWCASE_WIDGET_METADATA = {
|
|
|
2641
3228
|
type: 'PraxisChartPointEvent',
|
|
2642
3229
|
description: 'Forwards point selection from the chart or mapped row selection from the table.',
|
|
2643
3230
|
},
|
|
3231
|
+
{
|
|
3232
|
+
name: 'selectionChange',
|
|
3233
|
+
type: 'PraxisChartSelectionEvent',
|
|
3234
|
+
description: 'Forwards chart selection events with mapped filter values.',
|
|
3235
|
+
},
|
|
3236
|
+
{
|
|
3237
|
+
name: 'crossFilter',
|
|
3238
|
+
type: 'PraxisChartCrossFilterEvent',
|
|
3239
|
+
description: 'Forwards chart cross-filter events for Dynamic Page composition links.',
|
|
3240
|
+
},
|
|
2644
3241
|
{
|
|
2645
3242
|
name: 'queryRequest',
|
|
2646
3243
|
type: 'PraxisChartQueryRequestEvent',
|
|
@@ -2796,12 +3393,18 @@ class PraxisChartSchemaMapperService {
|
|
|
2796
3393
|
if (schema.outputs) {
|
|
2797
3394
|
return schema.outputs;
|
|
2798
3395
|
}
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
3396
|
+
const events = schema.chart?.events;
|
|
3397
|
+
const outputs = {};
|
|
3398
|
+
if (events?.pointClick || events?.drillDown) {
|
|
3399
|
+
outputs['pointClick'] = 'emit';
|
|
2803
3400
|
}
|
|
2804
|
-
|
|
3401
|
+
if (events?.selectionChange) {
|
|
3402
|
+
outputs['selectionChange'] = 'emit';
|
|
3403
|
+
}
|
|
3404
|
+
if (events?.crossFilter) {
|
|
3405
|
+
outputs['crossFilter'] = 'emit';
|
|
3406
|
+
}
|
|
3407
|
+
return Object.keys(outputs).length ? outputs : undefined;
|
|
2805
3408
|
}
|
|
2806
3409
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: PraxisChartSchemaMapperService, deps: [{ token: PraxisChartMetadataRegistrationService }, { token: PraxisChartCanonicalContractMapperService }], target: i0.ɵɵFactoryTarget.Injectable });
|
|
2807
3410
|
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: PraxisChartSchemaMapperService, providedIn: 'root' });
|
|
@@ -2973,6 +3576,7 @@ class AnalyticsChartConfigAdapterService {
|
|
|
2973
3576
|
case 'min':
|
|
2974
3577
|
case 'max':
|
|
2975
3578
|
case 'count':
|
|
3579
|
+
case 'distinct-count':
|
|
2976
3580
|
case 'sum':
|
|
2977
3581
|
return normalizedAggregation;
|
|
2978
3582
|
case '':
|
|
@@ -3130,7 +3734,7 @@ const PRAXIS_CHART_BACKEND_MOCK_BAR = {
|
|
|
3130
3734
|
chartId: 'monthly-revenue-chart',
|
|
3131
3735
|
title: { key: 'charts.heroes.universe.title', fallback: 'Heroes por universo' },
|
|
3132
3736
|
subtitle: { key: 'charts.heroes.universe.subtitle', fallback: 'Group-by em /vw-perfil-heroi' },
|
|
3133
|
-
height: 320,
|
|
3737
|
+
sizing: { mode: 'fixed', height: 320 },
|
|
3134
3738
|
source: {
|
|
3135
3739
|
kind: 'praxis.stats',
|
|
3136
3740
|
resource: 'api/human-resources/vw-perfil-heroi',
|
|
@@ -3320,7 +3924,7 @@ const PRAXIS_CHART_BACKEND_MOCK_HORIZONTAL_BAR = {
|
|
|
3320
3924
|
title: { key: 'charts.payroll.department.title', fallback: 'Massa salarial por departamento' },
|
|
3321
3925
|
subtitle: { key: 'charts.payroll.department.subtitle', fallback: 'Ranking horizontal consumindo praxis.stats' },
|
|
3322
3926
|
orientation: 'horizontal',
|
|
3323
|
-
height: 340,
|
|
3927
|
+
sizing: { mode: 'fixed', height: 340 },
|
|
3324
3928
|
source: {
|
|
3325
3929
|
kind: 'praxis.stats',
|
|
3326
3930
|
resource: 'api/human-resources/vw-analytics-folha-pagamento',
|
|
@@ -3381,7 +3985,7 @@ const PRAXIS_CHART_BACKEND_MOCK_STACKED_AREA = {
|
|
|
3381
3985
|
chartId: 'payroll-net-stacked-area',
|
|
3382
3986
|
title: { key: 'charts.payroll.stackedArea.title', fallback: 'Composicao temporal da folha' },
|
|
3383
3987
|
subtitle: { key: 'charts.payroll.stackedArea.subtitle', fallback: 'Stacked area sobre payroll analytics' },
|
|
3384
|
-
height: 340,
|
|
3988
|
+
sizing: { mode: 'fixed', height: 340 },
|
|
3385
3989
|
source: {
|
|
3386
3990
|
kind: 'praxis.stats',
|
|
3387
3991
|
resource: 'api/human-resources/vw-analytics-folha-pagamento',
|
|
@@ -3440,7 +4044,7 @@ const PRAXIS_CHART_BACKEND_MOCK_MULTI_METRIC_BAR = {
|
|
|
3440
4044
|
key: 'charts.payroll.multiMetric.subtitle',
|
|
3441
4045
|
fallback: 'Massa liquida e desconto medio via group-by sem eixo secundario',
|
|
3442
4046
|
},
|
|
3443
|
-
height: 340,
|
|
4047
|
+
sizing: { mode: 'fixed', height: 340 },
|
|
3444
4048
|
source: {
|
|
3445
4049
|
kind: 'praxis.stats',
|
|
3446
4050
|
resource: 'api/human-resources/vw-analytics-folha-pagamento',
|
|
@@ -3503,7 +4107,7 @@ const PRAXIS_CHART_BACKEND_MOCK_SCATTER = {
|
|
|
3503
4107
|
chartId: 'payroll-net-vs-discount-scatter',
|
|
3504
4108
|
title: { key: 'charts.payroll.scatter.title', fallback: 'Liquido x desconto medio' },
|
|
3505
4109
|
subtitle: { key: 'charts.payroll.scatter.subtitle', fallback: 'Scatter sobre recortes do payroll' },
|
|
3506
|
-
height: 340,
|
|
4110
|
+
sizing: { mode: 'fixed', height: 340 },
|
|
3507
4111
|
source: {
|
|
3508
4112
|
kind: 'praxis.stats',
|
|
3509
4113
|
resource: 'api/human-resources/vw-analytics-folha-pagamento',
|
|
@@ -3559,7 +4163,7 @@ const PRAXIS_CHART_BACKEND_MOCK_COMBO = {
|
|
|
3559
4163
|
chartId: 'payroll-combo-variance',
|
|
3560
4164
|
title: { key: 'charts.payroll.combo.title', fallback: 'Folha liquida e desconto medio' },
|
|
3561
4165
|
subtitle: { key: 'charts.payroll.combo.subtitle', fallback: 'Barras para liquido e linha para desconto medio via praxis.stats' },
|
|
3562
|
-
height: 340,
|
|
4166
|
+
sizing: { mode: 'fixed', height: 340 },
|
|
3563
4167
|
source: {
|
|
3564
4168
|
kind: 'praxis.stats',
|
|
3565
4169
|
resource: 'api/human-resources/vw-analytics-folha-pagamento',
|
|
@@ -4157,7 +4761,7 @@ class PraxisChartCompositionShowcaseComponent {
|
|
|
4157
4761
|
</article>
|
|
4158
4762
|
</div>
|
|
4159
4763
|
</section>
|
|
4160
|
-
`, isInline: true, styles: [":host{display:block}.showcase-shell{display:grid;gap:24px}.showcase-hero{display:grid;gap:20px;padding:28px;border-radius:28px;background:radial-gradient(circle at top right,rgba(18,99,180,.18),transparent 30%),linear-gradient(135deg,#071836f2,#1263b4c7);color:#f7fbff}.showcase-copy h2{margin:0 0 10px;font-size:clamp(1.8rem,3vw,2.6rem);line-height:1.05}.showcase-copy p{margin:0;max-width:52rem;color:#f7fbffe0}.showcase-eyebrow,.panel-kicker{margin:0 0 8px;font-size:.78rem;letter-spacing:.14em;text-transform:uppercase;color:#f7fbffb8}.showcase-controls{display:flex;flex-wrap:wrap;gap:16px}.control-group{display:grid;gap:8px}.control-group span{font-size:.84rem;color:#f7fbffc2}.control-buttons{display:flex;flex-wrap:wrap;gap:8px}.control-buttons button{border:1px solid rgba(247,251,255,.22);background:#f7fbff14;color:#f7fbff;border-radius:999px;padding:10px 14px;cursor:pointer;transition:background .16s ease,transform .16s ease}.control-buttons button.active{background:#f7fbff;color:#0d2d5f}.showcase-grid{display:grid;gap:20px;grid-template-columns:minmax(0,1.3fr) minmax(320px,.9fr)}.runtime-panel,.payload-panel{display:grid;gap:16px;padding:20px;border-radius:24px;border:1px solid color-mix(in srgb,var(--md-sys-color-outline, #c5c7ce) 76%,transparent);background:linear-gradient(180deg,#fffffff0,#f4f7fbf5)}.panel-header{display:flex;align-items:start;justify-content:space-between;gap:12px}.panel-header h3{margin:0;color:#142033}.panel-caption{margin:6px 0 0;color:#516074;font-size:.88rem;line-height:1.45}.panel-chip{border-radius:999px;padding:6px 10px;font-size:.76rem;background:#1263b41f;color:#1263b4}.payload-panel pre{margin:0;padding:16px;border-radius:18px;background:#09111f;color:#d7e6ff;overflow:auto;font-size:.84rem;line-height:1.5}@media(max-width:1080px){.showcase-grid{grid-template-columns:1fr}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: DynamicWidgetPageComponent, selector: "praxis-dynamic-page", inputs: ["page", "context", "strictValidation", "enableCustomization", "showPageSettingsButton", "shellEditorComponent", "pageEditorComponent", "autoPersist", "pageIdentity", "componentInstanceId"], outputs: ["pageChange", "widgetDiagnosticsChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
4764
|
+
`, isInline: true, styles: [":host{display:block}.showcase-shell{display:grid;gap:24px}.showcase-hero{display:grid;gap:20px;padding:28px;border-radius:28px;background:radial-gradient(circle at top right,rgba(18,99,180,.18),transparent 30%),linear-gradient(135deg,#071836f2,#1263b4c7);color:#f7fbff}.showcase-copy h2{margin:0 0 10px;font-size:clamp(1.8rem,3vw,2.6rem);line-height:1.05}.showcase-copy p{margin:0;max-width:52rem;color:#f7fbffe0}.showcase-eyebrow,.panel-kicker{margin:0 0 8px;font-size:.78rem;letter-spacing:.14em;text-transform:uppercase;color:#f7fbffb8}.showcase-controls{display:flex;flex-wrap:wrap;gap:16px}.control-group{display:grid;gap:8px}.control-group span{font-size:.84rem;color:#f7fbffc2}.control-buttons{display:flex;flex-wrap:wrap;gap:8px}.control-buttons button{border:1px solid rgba(247,251,255,.22);background:#f7fbff14;color:#f7fbff;border-radius:999px;padding:10px 14px;cursor:pointer;transition:background .16s ease,transform .16s ease}.control-buttons button.active{background:#f7fbff;color:#0d2d5f}.showcase-grid{display:grid;gap:20px;grid-template-columns:minmax(0,1.3fr) minmax(320px,.9fr)}.runtime-panel,.payload-panel{display:grid;gap:16px;padding:20px;border-radius:24px;border:1px solid color-mix(in srgb,var(--md-sys-color-outline, #c5c7ce) 76%,transparent);background:linear-gradient(180deg,#fffffff0,#f4f7fbf5)}.panel-header{display:flex;align-items:start;justify-content:space-between;gap:12px}.panel-header h3{margin:0;color:#142033}.panel-caption{margin:6px 0 0;color:#516074;font-size:.88rem;line-height:1.45}.panel-chip{border-radius:999px;padding:6px 10px;font-size:.76rem;background:#1263b41f;color:#1263b4}.payload-panel pre{margin:0;padding:16px;border-radius:18px;background:#09111f;color:#d7e6ff;overflow:auto;font-size:.84rem;line-height:1.5}@media(max-width:1080px){.showcase-grid{grid-template-columns:1fr}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: DynamicWidgetPageComponent, selector: "praxis-dynamic-page", inputs: ["page", "context", "strictValidation", "enableCustomization", "showPageSettingsButton", "shellEditorComponent", "pageEditorComponent", "autoPersist", "pageIdentity", "componentInstanceId"], outputs: ["pageChange", "widgetEvent", "widgetDiagnosticsChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
4161
4765
|
}
|
|
4162
4766
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: PraxisChartCompositionShowcaseComponent, decorators: [{
|
|
4163
4767
|
type: Component,
|
|
@@ -4279,7 +4883,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
|
|
|
4279
4883
|
const PRAXIS_CHARTS_EN_US = {
|
|
4280
4884
|
'praxis.charts.runtime.editChart': 'Edit chart settings',
|
|
4281
4885
|
'praxis.charts.runtime.invalidDocumentTitle': 'Invalid canonical configuration',
|
|
4282
|
-
'praxis.charts.runtime.invalidDocumentDescription': 'The canonical chart document could not be mapped
|
|
4886
|
+
'praxis.charts.runtime.invalidDocumentDescription': 'The canonical chart document could not be mapped by Praxis Charts. Review the chart contract before continuing.',
|
|
4283
4887
|
'praxis.charts.editor.section.general': 'General',
|
|
4284
4888
|
'praxis.charts.editor.section.data': 'Data',
|
|
4285
4889
|
'praxis.charts.editor.section.analytics': 'Analytics',
|
|
@@ -4291,7 +4895,11 @@ const PRAXIS_CHARTS_EN_US = {
|
|
|
4291
4895
|
'praxis.charts.editor.field.kind': 'Kind',
|
|
4292
4896
|
'praxis.charts.editor.field.title': 'Title',
|
|
4293
4897
|
'praxis.charts.editor.field.subtitle': 'Subtitle',
|
|
4898
|
+
'praxis.charts.editor.field.sizingMode': 'Sizing',
|
|
4294
4899
|
'praxis.charts.editor.field.height': 'Height',
|
|
4900
|
+
'praxis.charts.editor.field.minHeight': 'Minimum height',
|
|
4901
|
+
'praxis.charts.editor.field.maxHeight': 'Maximum height',
|
|
4902
|
+
'praxis.charts.editor.field.aspectRatio': 'Aspect ratio',
|
|
4295
4903
|
'praxis.charts.editor.field.sourceKind': 'Source',
|
|
4296
4904
|
'praxis.charts.editor.field.resource': 'Resource',
|
|
4297
4905
|
'praxis.charts.editor.field.operation': 'Operation',
|
|
@@ -4310,7 +4918,11 @@ const PRAXIS_CHARTS_EN_US = {
|
|
|
4310
4918
|
'praxis.charts.editor.field.legendEnabled': 'Show legend',
|
|
4311
4919
|
'praxis.charts.editor.field.labelsEnabled': 'Show labels',
|
|
4312
4920
|
'praxis.charts.editor.field.tooltipEnabled': 'Show tooltip',
|
|
4921
|
+
'praxis.charts.editor.field.themeVariant': 'Theme variant',
|
|
4922
|
+
'praxis.charts.editor.field.paletteMode': 'Palette mode',
|
|
4923
|
+
'praxis.charts.editor.field.paletteToken': 'Palette token',
|
|
4313
4924
|
'praxis.charts.editor.field.palette': 'Palette colors',
|
|
4925
|
+
'praxis.charts.editor.field.surfaceMode': 'Surface mode',
|
|
4314
4926
|
'praxis.charts.editor.field.emptyTitle': 'Empty title',
|
|
4315
4927
|
'praxis.charts.editor.field.emptyDescription': 'Empty description',
|
|
4316
4928
|
'praxis.charts.editor.field.loadingTitle': 'Loading title',
|
|
@@ -4329,8 +4941,15 @@ const PRAXIS_CHARTS_EN_US = {
|
|
|
4329
4941
|
'praxis.charts.editor.issues.empty': 'No issues were identified.',
|
|
4330
4942
|
'praxis.charts.editor.appearance.featuresTitle': 'Display features',
|
|
4331
4943
|
'praxis.charts.editor.appearance.paletteTitle': 'Palette',
|
|
4332
|
-
'praxis.charts.editor.appearance.paletteHint': 'Use
|
|
4944
|
+
'praxis.charts.editor.appearance.paletteHint': 'Use a registered token or comma-separated colors to persist theme.palette in the canonical contract.',
|
|
4945
|
+
'praxis.charts.editor.appearance.surfaceTitle': 'Surface',
|
|
4946
|
+
'praxis.charts.editor.appearance.surfaceHint': 'Use embedded for dashboard widgets and contained only when the chart must own its visual surface.',
|
|
4333
4947
|
'praxis.charts.editor.appearance.statesTitle': 'State messages',
|
|
4948
|
+
'praxis.charts.editor.hint.sizingMode': 'Use fill-container only when the host widget provides a defined body height.',
|
|
4949
|
+
'praxis.charts.editor.hint.height': 'Numbers are saved as pixels; CSS lengths such as 20rem are also accepted.',
|
|
4950
|
+
'praxis.charts.editor.hint.minHeight': 'Set a readable minimum for compact dashboard widgets.',
|
|
4951
|
+
'praxis.charts.editor.hint.maxHeight': 'Leave empty unless the chart must stop growing inside a flexible layout.',
|
|
4952
|
+
'praxis.charts.editor.hint.aspectRatio': 'Optional. Use values such as 1.777 or 16 / 9.',
|
|
4334
4953
|
'praxis.charts.editor.analytics.dimensionsTitle': 'Dimensions',
|
|
4335
4954
|
'praxis.charts.editor.analytics.metricsTitle': 'Metrics',
|
|
4336
4955
|
'praxis.charts.editor.analytics.addDimension': 'Add dimension',
|
|
@@ -4342,23 +4961,38 @@ const PRAXIS_CHARTS_EN_US = {
|
|
|
4342
4961
|
'praxis.charts.editor.specialization.comboTitle': 'Combo guidance',
|
|
4343
4962
|
'praxis.charts.editor.specialization.comboHint': 'Combo charts require at least two metrics and allow per-metric axis and series kind mapping.',
|
|
4344
4963
|
'praxis.charts.editor.specialization.pieDonutTitle': 'Composition guidance',
|
|
4964
|
+
'praxis.charts.editor.themeVariant.none': 'No variant',
|
|
4965
|
+
'praxis.charts.editor.themeVariant.default': 'Default',
|
|
4966
|
+
'praxis.charts.editor.themeVariant.compact': 'Compact',
|
|
4967
|
+
'praxis.charts.editor.themeVariant.executive': 'Executive',
|
|
4968
|
+
'praxis.charts.editor.paletteMode.token': 'Registered token',
|
|
4969
|
+
'praxis.charts.editor.paletteMode.custom': 'Custom colors',
|
|
4970
|
+
'praxis.charts.editor.paletteToken.brand-primary': 'Brand primary',
|
|
4971
|
+
'praxis.charts.editor.paletteToken.brand-balanced': 'Brand balanced',
|
|
4972
|
+
'praxis.charts.editor.paletteToken.status': 'Status',
|
|
4973
|
+
'praxis.charts.editor.paletteToken.executive': 'Executive',
|
|
4345
4974
|
'praxis.charts.editor.specialization.pieDonutHint': 'Pie and donut charts keep only the first metric and use the first dimension as the category segment.',
|
|
4346
4975
|
'praxis.charts.editor.specialization.scatterTitle': 'Scatter guidance',
|
|
4347
4976
|
'praxis.charts.editor.specialization.scatterHint': 'Scatter charts use the first dimension as X and the first metric as Y.',
|
|
4348
4977
|
'praxis.charts.editor.events.pointClickTitle': 'Point click',
|
|
4978
|
+
'praxis.charts.editor.events.selectionChangeTitle': 'Selection change',
|
|
4349
4979
|
'praxis.charts.editor.events.drillDownTitle': 'Drill down',
|
|
4980
|
+
'praxis.charts.editor.events.crossFilterTitle': 'Cross filter',
|
|
4350
4981
|
'praxis.charts.editor.events.none': 'None',
|
|
4351
4982
|
'praxis.charts.editor.sourceKind.praxisStats': 'praxis.stats',
|
|
4352
4983
|
'praxis.charts.editor.sourceKind.derived': 'derived',
|
|
4984
|
+
'praxis.charts.editor.sizing.fixed': 'Fixed height',
|
|
4985
|
+
'praxis.charts.editor.sizing.fill-container': 'Fill widget',
|
|
4986
|
+
'praxis.charts.editor.sizing.auto': 'Automatic',
|
|
4987
|
+
'praxis.charts.editor.surface.auto': 'Automatic',
|
|
4988
|
+
'praxis.charts.editor.surface.embedded': 'Embedded in widget',
|
|
4989
|
+
'praxis.charts.editor.surface.contained': 'Contained surface',
|
|
4353
4990
|
'praxis.charts.editor.operation.groupBy': 'group-by',
|
|
4354
4991
|
'praxis.charts.editor.operation.timeseries': 'timeseries',
|
|
4355
4992
|
'praxis.charts.editor.operation.distribution': 'distribution',
|
|
4356
|
-
'praxis.charts.editor.granularity.hour': 'hour',
|
|
4357
4993
|
'praxis.charts.editor.granularity.day': 'day',
|
|
4358
4994
|
'praxis.charts.editor.granularity.week': 'week',
|
|
4359
4995
|
'praxis.charts.editor.granularity.month': 'month',
|
|
4360
|
-
'praxis.charts.editor.granularity.quarter': 'quarter',
|
|
4361
|
-
'praxis.charts.editor.granularity.year': 'year',
|
|
4362
4996
|
'praxis.charts.editor.distributionMode.terms': 'terms',
|
|
4363
4997
|
'praxis.charts.editor.distributionMode.histogram': 'histogram',
|
|
4364
4998
|
'praxis.charts.editor.motionPreset.subtle': 'subtle',
|
|
@@ -4399,7 +5033,7 @@ const PRAXIS_CHARTS_EN_US = {
|
|
|
4399
5033
|
const PRAXIS_CHARTS_PT_BR = {
|
|
4400
5034
|
'praxis.charts.runtime.editChart': 'Editar configurações do gráfico',
|
|
4401
5035
|
'praxis.charts.runtime.invalidDocumentTitle': 'Configuração canônica inválida',
|
|
4402
|
-
'praxis.charts.runtime.invalidDocumentDescription': 'O documento canônico do gráfico não pode ser mapeado
|
|
5036
|
+
'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.',
|
|
4403
5037
|
'praxis.charts.editor.section.general': 'Geral',
|
|
4404
5038
|
'praxis.charts.editor.section.data': 'Dados',
|
|
4405
5039
|
'praxis.charts.editor.section.analytics': 'Estrutura',
|
|
@@ -4411,7 +5045,11 @@ const PRAXIS_CHARTS_PT_BR = {
|
|
|
4411
5045
|
'praxis.charts.editor.field.kind': 'Tipo',
|
|
4412
5046
|
'praxis.charts.editor.field.title': 'Título',
|
|
4413
5047
|
'praxis.charts.editor.field.subtitle': 'Subtítulo',
|
|
5048
|
+
'praxis.charts.editor.field.sizingMode': 'Tamanho',
|
|
4414
5049
|
'praxis.charts.editor.field.height': 'Altura',
|
|
5050
|
+
'praxis.charts.editor.field.minHeight': 'Altura mínima',
|
|
5051
|
+
'praxis.charts.editor.field.maxHeight': 'Altura máxima',
|
|
5052
|
+
'praxis.charts.editor.field.aspectRatio': 'Proporção',
|
|
4415
5053
|
'praxis.charts.editor.field.sourceKind': 'Fonte',
|
|
4416
5054
|
'praxis.charts.editor.field.resource': 'Recurso',
|
|
4417
5055
|
'praxis.charts.editor.field.operation': 'Operação',
|
|
@@ -4430,7 +5068,11 @@ const PRAXIS_CHARTS_PT_BR = {
|
|
|
4430
5068
|
'praxis.charts.editor.field.legendEnabled': 'Exibir legenda',
|
|
4431
5069
|
'praxis.charts.editor.field.labelsEnabled': 'Exibir rótulos',
|
|
4432
5070
|
'praxis.charts.editor.field.tooltipEnabled': 'Exibir dica de ferramenta (tooltip)',
|
|
5071
|
+
'praxis.charts.editor.field.themeVariant': 'Variante de tema',
|
|
5072
|
+
'praxis.charts.editor.field.paletteMode': 'Modo da paleta',
|
|
5073
|
+
'praxis.charts.editor.field.paletteToken': 'Token da paleta',
|
|
4433
5074
|
'praxis.charts.editor.field.palette': 'Cores da paleta',
|
|
5075
|
+
'praxis.charts.editor.field.surfaceMode': 'Modo da superfície',
|
|
4434
5076
|
'praxis.charts.editor.field.emptyTitle': 'Título do estado vazio',
|
|
4435
5077
|
'praxis.charts.editor.field.emptyDescription': 'Descrição do estado vazio',
|
|
4436
5078
|
'praxis.charts.editor.field.loadingTitle': 'Título de carregamento',
|
|
@@ -4449,8 +5091,15 @@ const PRAXIS_CHARTS_PT_BR = {
|
|
|
4449
5091
|
'praxis.charts.editor.issues.empty': 'Nenhum problema identificado.',
|
|
4450
5092
|
'praxis.charts.editor.appearance.featuresTitle': 'Recursos visuais',
|
|
4451
5093
|
'praxis.charts.editor.appearance.paletteTitle': 'Paleta',
|
|
4452
|
-
'praxis.charts.editor.appearance.paletteHint': 'Use cores separadas por vírgula
|
|
5094
|
+
'praxis.charts.editor.appearance.paletteHint': 'Use um token registrado ou cores separadas por vírgula para persistir `theme.palette` no contrato canônico.',
|
|
5095
|
+
'praxis.charts.editor.appearance.surfaceTitle': 'Superfície',
|
|
5096
|
+
'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.',
|
|
4453
5097
|
'praxis.charts.editor.appearance.statesTitle': 'Mensagens de estado',
|
|
5098
|
+
'praxis.charts.editor.hint.sizingMode': 'Use preencher container apenas quando o widget host fornecer altura definida para o corpo.',
|
|
5099
|
+
'praxis.charts.editor.hint.height': 'Números são salvos como pixels; medidas CSS como 20rem também são aceitas.',
|
|
5100
|
+
'praxis.charts.editor.hint.minHeight': 'Defina um mínimo legível para widgets compactos de dashboard.',
|
|
5101
|
+
'praxis.charts.editor.hint.maxHeight': 'Deixe vazio exceto quando o gráfico não puder crescer dentro de um layout flexível.',
|
|
5102
|
+
'praxis.charts.editor.hint.aspectRatio': 'Opcional. Use valores como 1.777 ou 16 / 9.',
|
|
4454
5103
|
'praxis.charts.editor.analytics.dimensionsTitle': 'Dimensões',
|
|
4455
5104
|
'praxis.charts.editor.analytics.metricsTitle': 'Métricas',
|
|
4456
5105
|
'praxis.charts.editor.analytics.addDimension': 'Adicionar dimensão',
|
|
@@ -4462,23 +5111,38 @@ const PRAXIS_CHARTS_PT_BR = {
|
|
|
4462
5111
|
'praxis.charts.editor.specialization.comboTitle': 'Guia de combo',
|
|
4463
5112
|
'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.',
|
|
4464
5113
|
'praxis.charts.editor.specialization.pieDonutTitle': 'Guia de composição',
|
|
5114
|
+
'praxis.charts.editor.themeVariant.none': 'Sem variante',
|
|
5115
|
+
'praxis.charts.editor.themeVariant.default': 'Padrão',
|
|
5116
|
+
'praxis.charts.editor.themeVariant.compact': 'Compacta',
|
|
5117
|
+
'praxis.charts.editor.themeVariant.executive': 'Executiva',
|
|
5118
|
+
'praxis.charts.editor.paletteMode.token': 'Token registrado',
|
|
5119
|
+
'praxis.charts.editor.paletteMode.custom': 'Cores customizadas',
|
|
5120
|
+
'praxis.charts.editor.paletteToken.brand-primary': 'Marca principal',
|
|
5121
|
+
'praxis.charts.editor.paletteToken.brand-balanced': 'Marca balanceada',
|
|
5122
|
+
'praxis.charts.editor.paletteToken.status': 'Status',
|
|
5123
|
+
'praxis.charts.editor.paletteToken.executive': 'Executiva',
|
|
4465
5124
|
'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.',
|
|
4466
5125
|
'praxis.charts.editor.specialization.scatterTitle': 'Guia de dispersão (scatter)',
|
|
4467
5126
|
'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.',
|
|
4468
5127
|
'praxis.charts.editor.events.pointClickTitle': 'Clique no ponto',
|
|
5128
|
+
'praxis.charts.editor.events.selectionChangeTitle': 'Mudança de seleção',
|
|
4469
5129
|
'praxis.charts.editor.events.drillDownTitle': 'Drill down',
|
|
5130
|
+
'praxis.charts.editor.events.crossFilterTitle': 'Filtro cruzado',
|
|
4470
5131
|
'praxis.charts.editor.events.none': 'Nenhuma',
|
|
4471
5132
|
'praxis.charts.editor.sourceKind.praxisStats': 'praxis.stats',
|
|
4472
5133
|
'praxis.charts.editor.sourceKind.derived': 'derivado',
|
|
5134
|
+
'praxis.charts.editor.sizing.fixed': 'Altura fixa',
|
|
5135
|
+
'praxis.charts.editor.sizing.fill-container': 'Preencher widget',
|
|
5136
|
+
'praxis.charts.editor.sizing.auto': 'Automático',
|
|
5137
|
+
'praxis.charts.editor.surface.auto': 'Automático',
|
|
5138
|
+
'praxis.charts.editor.surface.embedded': 'Embutido no widget',
|
|
5139
|
+
'praxis.charts.editor.surface.contained': 'Superfície contida',
|
|
4473
5140
|
'praxis.charts.editor.operation.groupBy': 'agrupar por',
|
|
4474
5141
|
'praxis.charts.editor.operation.timeseries': 'série temporal',
|
|
4475
5142
|
'praxis.charts.editor.operation.distribution': 'distribuição',
|
|
4476
|
-
'praxis.charts.editor.granularity.hour': 'hora',
|
|
4477
5143
|
'praxis.charts.editor.granularity.day': 'dia',
|
|
4478
5144
|
'praxis.charts.editor.granularity.week': 'semana',
|
|
4479
5145
|
'praxis.charts.editor.granularity.month': 'mês',
|
|
4480
|
-
'praxis.charts.editor.granularity.quarter': 'trimestre',
|
|
4481
|
-
'praxis.charts.editor.granularity.year': 'ano',
|
|
4482
5146
|
'praxis.charts.editor.distributionMode.terms': 'termos',
|
|
4483
5147
|
'praxis.charts.editor.distributionMode.histogram': 'histograma',
|
|
4484
5148
|
'praxis.charts.editor.motionPreset.subtle': 'sutil',
|
|
@@ -4682,14 +5346,19 @@ class PraxisChartConfigEditor {
|
|
|
4682
5346
|
];
|
|
4683
5347
|
sourceKinds = ['praxis.stats', 'derived'];
|
|
4684
5348
|
operations = ['group-by', 'timeseries', 'distribution'];
|
|
4685
|
-
timeGranularities = ['
|
|
5349
|
+
timeGranularities = ['day', 'week', 'month'];
|
|
4686
5350
|
distributionModes = ['terms', 'histogram'];
|
|
4687
5351
|
dimensionRoles = ['category', 'series', 'segment', 'time', 'value'];
|
|
4688
|
-
metricAggregations = ['sum', 'count', 'avg', 'min', 'max'];
|
|
5352
|
+
metricAggregations = ['sum', 'count', 'distinct-count', 'avg', 'min', 'max'];
|
|
4689
5353
|
metricAxes = ['primary', 'secondary'];
|
|
4690
5354
|
metricSeriesKinds = ['bar', 'line', 'area'];
|
|
4691
5355
|
motionPresets = ['subtle', 'standard', 'expressive'];
|
|
4692
5356
|
eventActionOptions = ['filter-widget', 'open-detail', 'navigate', 'update-context', 'emit'];
|
|
5357
|
+
sizingModes = ['fixed', 'fill-container', 'auto'];
|
|
5358
|
+
surfaceModes = ['auto', 'embedded', 'contained'];
|
|
5359
|
+
themeVariants = ['default', 'compact', 'executive'];
|
|
5360
|
+
paletteModes = ['token', 'custom'];
|
|
5361
|
+
paletteTokens = Object.keys(PRAXIS_CHART_PALETTE_TOKENS);
|
|
4693
5362
|
activeSection = signal('general', ...(ngDevMode ? [{ debugName: "activeSection" }] : []));
|
|
4694
5363
|
injectedData = inject(SETTINGS_PANEL_DATA, { optional: true });
|
|
4695
5364
|
defaults = inject(ChartEditorDefaultsService);
|
|
@@ -4790,9 +5459,62 @@ class PraxisChartConfigEditor {
|
|
|
4790
5459
|
}));
|
|
4791
5460
|
}
|
|
4792
5461
|
setHeight(value) {
|
|
5462
|
+
this.setSizingHeight(value);
|
|
5463
|
+
}
|
|
5464
|
+
setSizingMode(mode) {
|
|
5465
|
+
this.patchDocument((document) => ({
|
|
5466
|
+
...document,
|
|
5467
|
+
height: undefined,
|
|
5468
|
+
sizing: {
|
|
5469
|
+
...(document.sizing ?? {}),
|
|
5470
|
+
mode,
|
|
5471
|
+
height: mode === 'fixed' ? document.sizing?.height : undefined,
|
|
5472
|
+
},
|
|
5473
|
+
}));
|
|
5474
|
+
}
|
|
5475
|
+
setSizingHeight(value) {
|
|
5476
|
+
const height = this.normalizeSizingSizeInput(value);
|
|
5477
|
+
this.patchDocument((document) => ({
|
|
5478
|
+
...document,
|
|
5479
|
+
height: undefined,
|
|
5480
|
+
sizing: {
|
|
5481
|
+
...(document.sizing ?? {}),
|
|
5482
|
+
mode: 'fixed',
|
|
5483
|
+
height,
|
|
5484
|
+
},
|
|
5485
|
+
}));
|
|
5486
|
+
}
|
|
5487
|
+
setSizingMinHeight(value) {
|
|
5488
|
+
const minHeight = this.normalizeSizingSizeInput(value);
|
|
4793
5489
|
this.patchDocument((document) => ({
|
|
4794
5490
|
...document,
|
|
4795
|
-
height:
|
|
5491
|
+
height: undefined,
|
|
5492
|
+
sizing: {
|
|
5493
|
+
...(document.sizing ?? {}),
|
|
5494
|
+
minHeight,
|
|
5495
|
+
},
|
|
5496
|
+
}));
|
|
5497
|
+
}
|
|
5498
|
+
setSizingMaxHeight(value) {
|
|
5499
|
+
const maxHeight = this.normalizeSizingSizeInput(value);
|
|
5500
|
+
this.patchDocument((document) => ({
|
|
5501
|
+
...document,
|
|
5502
|
+
height: undefined,
|
|
5503
|
+
sizing: {
|
|
5504
|
+
...(document.sizing ?? {}),
|
|
5505
|
+
maxHeight,
|
|
5506
|
+
},
|
|
5507
|
+
}));
|
|
5508
|
+
}
|
|
5509
|
+
setSizingAspectRatio(value) {
|
|
5510
|
+
const aspectRatio = this.normalizeSizingSizeInput(value);
|
|
5511
|
+
this.patchDocument((document) => ({
|
|
5512
|
+
...document,
|
|
5513
|
+
height: undefined,
|
|
5514
|
+
sizing: {
|
|
5515
|
+
...(document.sizing ?? {}),
|
|
5516
|
+
aspectRatio,
|
|
5517
|
+
},
|
|
4796
5518
|
}));
|
|
4797
5519
|
}
|
|
4798
5520
|
setSourceKind(value) {
|
|
@@ -5015,6 +5737,30 @@ class PraxisChartConfigEditor {
|
|
|
5015
5737
|
},
|
|
5016
5738
|
}));
|
|
5017
5739
|
}
|
|
5740
|
+
setThemeVariant(variant) {
|
|
5741
|
+
this.patchDocument((document) => ({
|
|
5742
|
+
...document,
|
|
5743
|
+
theme: this.mergeTheme(document.theme, {
|
|
5744
|
+
variant: variant || undefined,
|
|
5745
|
+
}),
|
|
5746
|
+
}));
|
|
5747
|
+
}
|
|
5748
|
+
setPaletteMode(mode) {
|
|
5749
|
+
this.patchDocument((document) => ({
|
|
5750
|
+
...document,
|
|
5751
|
+
theme: this.mergeTheme(document.theme, {
|
|
5752
|
+
palette: mode === 'token' ? 'brand-primary' : undefined,
|
|
5753
|
+
}),
|
|
5754
|
+
}));
|
|
5755
|
+
}
|
|
5756
|
+
setPaletteToken(token) {
|
|
5757
|
+
this.patchDocument((document) => ({
|
|
5758
|
+
...document,
|
|
5759
|
+
theme: this.mergeTheme(document.theme, {
|
|
5760
|
+
palette: token,
|
|
5761
|
+
}),
|
|
5762
|
+
}));
|
|
5763
|
+
}
|
|
5018
5764
|
setPalette(value) {
|
|
5019
5765
|
const palette = value
|
|
5020
5766
|
.split(/[\n,]/)
|
|
@@ -5022,10 +5768,20 @@ class PraxisChartConfigEditor {
|
|
|
5022
5768
|
.filter(Boolean);
|
|
5023
5769
|
this.patchDocument((document) => ({
|
|
5024
5770
|
...document,
|
|
5025
|
-
theme: {
|
|
5026
|
-
...(document.theme ?? {}),
|
|
5771
|
+
theme: this.mergeTheme(document.theme, {
|
|
5027
5772
|
palette: palette.length ? palette : undefined,
|
|
5028
|
-
},
|
|
5773
|
+
}),
|
|
5774
|
+
}));
|
|
5775
|
+
}
|
|
5776
|
+
setSurfaceMode(mode) {
|
|
5777
|
+
this.patchDocument((document) => ({
|
|
5778
|
+
...document,
|
|
5779
|
+
theme: this.mergeTheme(document.theme, {
|
|
5780
|
+
surface: {
|
|
5781
|
+
...(document.theme?.surface ?? {}),
|
|
5782
|
+
mode,
|
|
5783
|
+
},
|
|
5784
|
+
}),
|
|
5029
5785
|
}));
|
|
5030
5786
|
}
|
|
5031
5787
|
setStateTitle(stateKey, value) {
|
|
@@ -5130,9 +5886,25 @@ class PraxisChartConfigEditor {
|
|
|
5130
5886
|
return value === undefined ? '' : String(value);
|
|
5131
5887
|
}
|
|
5132
5888
|
heightValue() {
|
|
5133
|
-
const
|
|
5889
|
+
const document = this.normalizedDocument();
|
|
5890
|
+
const height = document.sizing?.height;
|
|
5134
5891
|
return height === undefined || height === null ? '' : String(height);
|
|
5135
5892
|
}
|
|
5893
|
+
sizingModeValue() {
|
|
5894
|
+
return this.normalizedDocument().sizing?.mode ?? 'auto';
|
|
5895
|
+
}
|
|
5896
|
+
sizingMinHeightValue() {
|
|
5897
|
+
const minHeight = this.normalizedDocument().sizing?.minHeight;
|
|
5898
|
+
return minHeight === undefined || minHeight === null ? '' : String(minHeight);
|
|
5899
|
+
}
|
|
5900
|
+
sizingMaxHeightValue() {
|
|
5901
|
+
const maxHeight = this.normalizedDocument().sizing?.maxHeight;
|
|
5902
|
+
return maxHeight === undefined || maxHeight === null ? '' : String(maxHeight);
|
|
5903
|
+
}
|
|
5904
|
+
sizingAspectRatioValue() {
|
|
5905
|
+
const aspectRatio = this.normalizedDocument().sizing?.aspectRatio;
|
|
5906
|
+
return aspectRatio === undefined || aspectRatio === null ? '' : String(aspectRatio);
|
|
5907
|
+
}
|
|
5136
5908
|
featureEnabled(feature) {
|
|
5137
5909
|
const value = this.doc()[feature];
|
|
5138
5910
|
if (typeof value === 'boolean') {
|
|
@@ -5144,6 +5916,21 @@ class PraxisChartConfigEditor {
|
|
|
5144
5916
|
const palette = this.doc().theme?.palette;
|
|
5145
5917
|
return Array.isArray(palette) ? palette.join(', ') : '';
|
|
5146
5918
|
}
|
|
5919
|
+
paletteModeValue() {
|
|
5920
|
+
return typeof this.doc().theme?.palette === 'string' ? 'token' : 'custom';
|
|
5921
|
+
}
|
|
5922
|
+
paletteTokenValue() {
|
|
5923
|
+
const palette = this.doc().theme?.palette;
|
|
5924
|
+
return typeof palette === 'string' && this.paletteTokens.includes(palette)
|
|
5925
|
+
? palette
|
|
5926
|
+
: 'brand-primary';
|
|
5927
|
+
}
|
|
5928
|
+
themeVariantValue() {
|
|
5929
|
+
return this.normalizedDocument().theme?.variant ?? '';
|
|
5930
|
+
}
|
|
5931
|
+
surfaceModeValue() {
|
|
5932
|
+
return this.normalizedDocument().theme?.surface?.mode ?? 'auto';
|
|
5933
|
+
}
|
|
5147
5934
|
stateTitle(stateKey) {
|
|
5148
5935
|
const value = this.doc().state?.[stateKey]?.title;
|
|
5149
5936
|
if (typeof value === 'string') {
|
|
@@ -5267,6 +6054,22 @@ class PraxisChartConfigEditor {
|
|
|
5267
6054
|
return accumulator;
|
|
5268
6055
|
}, {});
|
|
5269
6056
|
}
|
|
6057
|
+
normalizeSizingSizeInput(value) {
|
|
6058
|
+
const trimmed = value.trim();
|
|
6059
|
+
if (!trimmed) {
|
|
6060
|
+
return undefined;
|
|
6061
|
+
}
|
|
6062
|
+
return /^\d+(\.\d+)?$/.test(trimmed) ? Number(trimmed) : trimmed;
|
|
6063
|
+
}
|
|
6064
|
+
mergeTheme(currentTheme, patch) {
|
|
6065
|
+
const nextTheme = {
|
|
6066
|
+
...(currentTheme ?? {}),
|
|
6067
|
+
...(patch ?? {}),
|
|
6068
|
+
};
|
|
6069
|
+
return Object.values(nextTheme).some((value) => value !== undefined)
|
|
6070
|
+
? nextTheme
|
|
6071
|
+
: undefined;
|
|
6072
|
+
}
|
|
5270
6073
|
createApplyPayload() {
|
|
5271
6074
|
return {
|
|
5272
6075
|
document: this.getSettingsValue(),
|
|
@@ -5294,7 +6097,7 @@ class PraxisChartConfigEditor {
|
|
|
5294
6097
|
this.documentChange.emit(structuredClone(this.normalizedDocument()));
|
|
5295
6098
|
}
|
|
5296
6099
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: PraxisChartConfigEditor, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
5297
|
-
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 });
|
|
6100
|
+
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.selectionChangeTitle', 'Selection change') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventAction', 'Action') }}</mat-label>\n <mat-select [ngModel]=\"eventAction('selectionChange')\" (ngModelChange)=\"setEventAction('selectionChange', $event)\" [disabled]=\"isReadonly()\">\n <mat-option value=\"\">{{ t('praxis.charts.editor.events.none', 'None') }}</mat-option>\n @for (action of eventActionOptions; track action) {\n <mat-option [value]=\"action\">\n {{ t('praxis.charts.editor.eventAction.' + action, action) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventTarget', 'Target') }}</mat-label>\n @if (targetOptions().length) {\n <mat-select [ngModel]=\"eventTarget('selectionChange')\" (ngModelChange)=\"setEventTarget('selectionChange', $event)\" [disabled]=\"isReadonly() || !eventAction('selectionChange')\">\n @for (target of targetOptions(); track target.id) {\n <mat-option [value]=\"target.id\">{{ target.label }}</mat-option>\n }\n </mat-select>\n } @else {\n <input matInput [ngModel]=\"eventTarget('selectionChange')\" (ngModelChange)=\"setEventTarget('selectionChange', $event)\" [disabled]=\"isReadonly() || !eventAction('selectionChange')\" />\n }\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventMapping', 'Mapping') }}</mat-label>\n <textarea\n matInput\n rows=\"4\"\n [ngModel]=\"eventMappingText('selectionChange')\"\n (ngModelChange)=\"setEventMapping('selectionChange', $event)\"\n [disabled]=\"isReadonly() || !eventAction('selectionChange')\"\n ></textarea>\n </mat-form-field>\n </mat-card-content>\n </mat-card>\n\n <mat-card class=\"editor-card\">\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\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.events.crossFilterTitle', 'Cross filter') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventAction', 'Action') }}</mat-label>\n <mat-select [ngModel]=\"eventAction('crossFilter')\" (ngModelChange)=\"setEventAction('crossFilter', $event)\" [disabled]=\"isReadonly()\">\n <mat-option value=\"\">{{ t('praxis.charts.editor.events.none', 'None') }}</mat-option>\n @for (action of eventActionOptions; track action) {\n <mat-option [value]=\"action\">\n {{ t('praxis.charts.editor.eventAction.' + action, action) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventTarget', 'Target') }}</mat-label>\n @if (targetOptions().length) {\n <mat-select [ngModel]=\"eventTarget('crossFilter')\" (ngModelChange)=\"setEventTarget('crossFilter', $event)\" [disabled]=\"isReadonly() || !eventAction('crossFilter')\">\n @for (target of targetOptions(); track target.id) {\n <mat-option [value]=\"target.id\">{{ target.label }}</mat-option>\n }\n </mat-select>\n } @else {\n <input matInput [ngModel]=\"eventTarget('crossFilter')\" (ngModelChange)=\"setEventTarget('crossFilter', $event)\" [disabled]=\"isReadonly() || !eventAction('crossFilter')\" />\n }\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventMapping', 'Mapping') }}</mat-label>\n <textarea\n matInput\n rows=\"4\"\n [ngModel]=\"eventMappingText('crossFilter')\"\n (ngModelChange)=\"setEventMapping('crossFilter', $event)\"\n [disabled]=\"isReadonly() || !eventAction('crossFilter')\"\n ></textarea>\n </mat-form-field>\n </mat-card-content>\n </mat-card>\n </div>\n }\n\n @case ('preview') {\n <p class=\"editor-caption\">\n {{ t('praxis.charts.editor.preview.caption', 'Local preview derived from the canonical contract without remote calls.') }}\n </p>\n }\n }\n </mat-card-content>\n </mat-card>\n </div>\n\n <div class=\"editor-side\">\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.issues.title', 'Validation issues') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content>\n @if (issues().length) {\n <ul class=\"editor-issues\">\n @for (issue of issues(); track issueTrackBy($index, issue)) {\n <li class=\"editor-issue\">\n <strong>{{ issue.field }}</strong>\n <span>{{ issue.message }}</span>\n </li>\n }\n </ul>\n } @else {\n <div class=\"editor-empty\">\n {{ t('praxis.charts.editor.issues.empty', 'No issues were identified.') }}\n </div>\n }\n </mat-card-content>\n </mat-card>\n\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.preview.title', 'Chart preview') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content>\n @if (preview(); as chartPreview) {\n <p class=\"editor-caption\">\n {{ t('praxis.charts.editor.preview.caption', 'Local preview derived from the canonical contract without remote calls.') }}\n </p>\n <praxis-chart [config]=\"chartPreview.config\" [data]=\"chartPreview.data\"></praxis-chart>\n } @else {\n <div class=\"editor-empty\">\n {{ t('praxis.charts.editor.preview.invalid', 'Preview is unavailable while the contract has blocking errors.') }}\n </div>\n }\n </mat-card-content>\n </mat-card>\n </div>\n </div>\n</div>\n", styles: [":host{display:block;min-width:0;color:var(--md-sys-color-on-surface, #1a1b20)}.editor-shell{display:grid;gap:18px}.editor-nav{display:flex;gap:8px;flex-wrap:wrap}.editor-nav button.active{background:color-mix(in srgb,var(--md-sys-color-primary, #1263b4) 18%,transparent);color:var(--md-sys-color-primary, #1263b4)}.editor-layout{display:grid;gap:18px;grid-template-columns:minmax(0,1.35fr) minmax(320px,.9fr);align-items:start}.editor-form,.editor-side{display:grid;gap:16px}.editor-card{border-radius:20px;border:1px solid color-mix(in srgb,var(--md-sys-color-outline, #c5c7ce) 72%,transparent);background:linear-gradient(180deg,#1263b408,#1263b400)}.editor-grid{display:grid;gap:14px;grid-template-columns:repeat(auto-fit,minmax(220px,1fr))}.editor-stack{display:grid;gap:14px}.editor-row-card{padding:14px;border-radius:16px;border:1px solid color-mix(in srgb,var(--md-sys-color-outline, #c5c7ce) 54%,transparent);background:color-mix(in srgb,var(--md-sys-color-surface, #fff) 92%,rgba(18,99,180,.04))}.editor-row-actions{display:flex;justify-content:flex-end}.editor-field{width:100%}.editor-issues{display:grid;gap:10px;margin:0;padding:0;list-style:none}.editor-issue{padding:12px 14px;border-radius:14px;background:color-mix(in srgb,var(--md-sys-color-error, #b3261e) 8%,transparent);border:1px solid color-mix(in srgb,var(--md-sys-color-error, #b3261e) 18%,transparent)}.editor-issue strong{display:block;margin-bottom:4px}.editor-caption{margin:0 0 12px;color:var(--md-sys-color-on-surface-variant, #5a5d67);font-size:.92rem}.editor-empty{padding:18px;border-radius:14px;background:color-mix(in srgb,var(--md-sys-color-surface-variant, #eceff4) 78%,transparent)}@media(max-width:960px){.editor-layout{grid-template-columns:minmax(0,1fr)}}\n"], dependencies: [{ kind: "ngmodule", type: 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", "remoteDataResolver", "enableCustomization", "availableResources", "availableFields", "availableTargets"], outputs: ["pointClick", "selectionChange", "crossFilter", "queryRequest", "loadStateChange", "chartDocumentApplied", "chartDocumentSaved"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
5298
6101
|
}
|
|
5299
6102
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: PraxisChartConfigEditor, decorators: [{
|
|
5300
6103
|
type: Component,
|
|
@@ -5308,7 +6111,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
|
|
|
5308
6111
|
MatSelectModule,
|
|
5309
6112
|
MatSlideToggleModule,
|
|
5310
6113
|
PraxisChartComponent,
|
|
5311
|
-
], 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"] }]
|
|
6114
|
+
], 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.selectionChangeTitle', 'Selection change') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventAction', 'Action') }}</mat-label>\n <mat-select [ngModel]=\"eventAction('selectionChange')\" (ngModelChange)=\"setEventAction('selectionChange', $event)\" [disabled]=\"isReadonly()\">\n <mat-option value=\"\">{{ t('praxis.charts.editor.events.none', 'None') }}</mat-option>\n @for (action of eventActionOptions; track action) {\n <mat-option [value]=\"action\">\n {{ t('praxis.charts.editor.eventAction.' + action, action) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventTarget', 'Target') }}</mat-label>\n @if (targetOptions().length) {\n <mat-select [ngModel]=\"eventTarget('selectionChange')\" (ngModelChange)=\"setEventTarget('selectionChange', $event)\" [disabled]=\"isReadonly() || !eventAction('selectionChange')\">\n @for (target of targetOptions(); track target.id) {\n <mat-option [value]=\"target.id\">{{ target.label }}</mat-option>\n }\n </mat-select>\n } @else {\n <input matInput [ngModel]=\"eventTarget('selectionChange')\" (ngModelChange)=\"setEventTarget('selectionChange', $event)\" [disabled]=\"isReadonly() || !eventAction('selectionChange')\" />\n }\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventMapping', 'Mapping') }}</mat-label>\n <textarea\n matInput\n rows=\"4\"\n [ngModel]=\"eventMappingText('selectionChange')\"\n (ngModelChange)=\"setEventMapping('selectionChange', $event)\"\n [disabled]=\"isReadonly() || !eventAction('selectionChange')\"\n ></textarea>\n </mat-form-field>\n </mat-card-content>\n </mat-card>\n\n <mat-card class=\"editor-card\">\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\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.events.crossFilterTitle', 'Cross filter') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventAction', 'Action') }}</mat-label>\n <mat-select [ngModel]=\"eventAction('crossFilter')\" (ngModelChange)=\"setEventAction('crossFilter', $event)\" [disabled]=\"isReadonly()\">\n <mat-option value=\"\">{{ t('praxis.charts.editor.events.none', 'None') }}</mat-option>\n @for (action of eventActionOptions; track action) {\n <mat-option [value]=\"action\">\n {{ t('praxis.charts.editor.eventAction.' + action, action) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventTarget', 'Target') }}</mat-label>\n @if (targetOptions().length) {\n <mat-select [ngModel]=\"eventTarget('crossFilter')\" (ngModelChange)=\"setEventTarget('crossFilter', $event)\" [disabled]=\"isReadonly() || !eventAction('crossFilter')\">\n @for (target of targetOptions(); track target.id) {\n <mat-option [value]=\"target.id\">{{ target.label }}</mat-option>\n }\n </mat-select>\n } @else {\n <input matInput [ngModel]=\"eventTarget('crossFilter')\" (ngModelChange)=\"setEventTarget('crossFilter', $event)\" [disabled]=\"isReadonly() || !eventAction('crossFilter')\" />\n }\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventMapping', 'Mapping') }}</mat-label>\n <textarea\n matInput\n rows=\"4\"\n [ngModel]=\"eventMappingText('crossFilter')\"\n (ngModelChange)=\"setEventMapping('crossFilter', $event)\"\n [disabled]=\"isReadonly() || !eventAction('crossFilter')\"\n ></textarea>\n </mat-form-field>\n </mat-card-content>\n </mat-card>\n </div>\n }\n\n @case ('preview') {\n <p class=\"editor-caption\">\n {{ t('praxis.charts.editor.preview.caption', 'Local preview derived from the canonical contract without remote calls.') }}\n </p>\n }\n }\n </mat-card-content>\n </mat-card>\n </div>\n\n <div class=\"editor-side\">\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.issues.title', 'Validation issues') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content>\n @if (issues().length) {\n <ul class=\"editor-issues\">\n @for (issue of issues(); track issueTrackBy($index, issue)) {\n <li class=\"editor-issue\">\n <strong>{{ issue.field }}</strong>\n <span>{{ issue.message }}</span>\n </li>\n }\n </ul>\n } @else {\n <div class=\"editor-empty\">\n {{ t('praxis.charts.editor.issues.empty', 'No issues were identified.') }}\n </div>\n }\n </mat-card-content>\n </mat-card>\n\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.preview.title', 'Chart preview') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content>\n @if (preview(); as chartPreview) {\n <p class=\"editor-caption\">\n {{ t('praxis.charts.editor.preview.caption', 'Local preview derived from the canonical contract without remote calls.') }}\n </p>\n <praxis-chart [config]=\"chartPreview.config\" [data]=\"chartPreview.data\"></praxis-chart>\n } @else {\n <div class=\"editor-empty\">\n {{ t('praxis.charts.editor.preview.invalid', 'Preview is unavailable while the contract has blocking errors.') }}\n </div>\n }\n </mat-card-content>\n </mat-card>\n </div>\n </div>\n</div>\n", styles: [":host{display:block;min-width:0;color:var(--md-sys-color-on-surface, #1a1b20)}.editor-shell{display:grid;gap:18px}.editor-nav{display:flex;gap:8px;flex-wrap:wrap}.editor-nav button.active{background:color-mix(in srgb,var(--md-sys-color-primary, #1263b4) 18%,transparent);color:var(--md-sys-color-primary, #1263b4)}.editor-layout{display:grid;gap:18px;grid-template-columns:minmax(0,1.35fr) minmax(320px,.9fr);align-items:start}.editor-form,.editor-side{display:grid;gap:16px}.editor-card{border-radius:20px;border:1px solid color-mix(in srgb,var(--md-sys-color-outline, #c5c7ce) 72%,transparent);background:linear-gradient(180deg,#1263b408,#1263b400)}.editor-grid{display:grid;gap:14px;grid-template-columns:repeat(auto-fit,minmax(220px,1fr))}.editor-stack{display:grid;gap:14px}.editor-row-card{padding:14px;border-radius:16px;border:1px solid color-mix(in srgb,var(--md-sys-color-outline, #c5c7ce) 54%,transparent);background:color-mix(in srgb,var(--md-sys-color-surface, #fff) 92%,rgba(18,99,180,.04))}.editor-row-actions{display:flex;justify-content:flex-end}.editor-field{width:100%}.editor-issues{display:grid;gap:10px;margin:0;padding:0;list-style:none}.editor-issue{padding:12px 14px;border-radius:14px;background:color-mix(in srgb,var(--md-sys-color-error, #b3261e) 8%,transparent);border:1px solid color-mix(in srgb,var(--md-sys-color-error, #b3261e) 18%,transparent)}.editor-issue strong{display:block;margin-bottom:4px}.editor-caption{margin:0 0 12px;color:var(--md-sys-color-on-surface-variant, #5a5d67);font-size:.92rem}.editor-empty{padding:18px;border-radius:14px;background:color-mix(in srgb,var(--md-sys-color-surface-variant, #eceff4) 78%,transparent)}@media(max-width:960px){.editor-layout{grid-template-columns:minmax(0,1fr)}}\n"] }]
|
|
5312
6115
|
}], 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"] }] } });
|
|
5313
6116
|
|
|
5314
6117
|
var praxisChartConfigEditor = /*#__PURE__*/Object.freeze({
|
|
@@ -5324,4 +6127,4 @@ var praxisChartConfigEditor = /*#__PURE__*/Object.freeze({
|
|
|
5324
6127
|
* Generated bundle index. Do not edit.
|
|
5325
6128
|
*/
|
|
5326
6129
|
|
|
5327
|
-
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 };
|
|
6130
|
+
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 };
|