@praxisui/charts 10.0.0-rc.20 → 10.0.0-rc.21

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 CHANGED
@@ -311,15 +311,17 @@ The Dynamic Page Lab's preset studio includes “Testar com componentes reais”
311
311
 
312
312
  `PraxisChartConfigEditor` is the initial editor shell for canonical `x-ui.chart` documents. It consumes governed resources, fields and targets and emits structured apply/save/reset events. The authoring manifest is exported as `PRAXIS_CHARTS_AUTHORING_MANIFEST` for backend/tooling workflows that need executable chart-edit operations.
313
313
 
314
- For the direct runtime editor, Apply updates the chart and emits `chartDocumentApplied`; Save emits `chartDocumentSaved`. Both also emit `chartDocumentPatchChange` with an independent `inputPatch.chartDocument` snapshot for dynamic widget hosts. Dynamic Page/Page Builder updates its authored page, so Apply followed by Save Page preserves the canonical document. Apply alone does not persist the page remotely. Unrelated widget inputs and transient catalogs remain owned by their existing host/context boundaries.
314
+ The direct runtime host opens `PraxisChartWidgetConfigEditor` with the existing `{ inputs }` envelope so the canonical document, derived `data` and separate `queryContext` share the Apply/Save boundary. Apply updates the runtime and emits `chartDocumentApplied`; Save emits `chartDocumentSaved`. Both document outputs retain their `PraxisXUiChartContract` shape. `chartDocumentPatchChange` carries independent snapshots in `inputPatch.chartDocument`, `data` and `queryContext` when applicable, plus mapped `config` for a config-driven host. Switching to `praxis.stats` clears local data precedence with `inputPatch.data: null`; clearing query context uses `queryContext: null`.
315
+
316
+ The accepted editor state belongs to the opening owner inputs. An owner may echo the latest accepted value of each input one at a time, including fields omitted from a later patch; equivalent clones do not imply a new owner. Hosts must apply patches in order: an older value of the same input delivered after a newer accepted value is indistinguishable from an external override and is treated as one. A distinct external input change replaces the local document/data/context state, and old editor events are rejected even before the next render. Closing or destroying the owner releases the editor subscriptions. Cancel discards unapplied editor work; it does not undo a previous Apply. Dynamic Page/Page Builder can materialize the patch in its authored page, but external persistence remains the host's responsibility. Apply or Save alone is not proof of remote persistence; the standalone showcase keeps runtime state only. Transient catalogs remain outside the persisted input envelope.
315
317
 
316
318
  `ChartResourceCapabilityCatalogAdapter` derives resource operations and eligible dimension, time and metric options directly from `ResourceCapabilitySnapshot`, including fail-closed comparison eligibility.
317
319
 
318
- Applications should install `providePraxisCharts()` once. The registered `ComponentDocMeta.configEditor.contextResolver` materializes the editor context without making Page Builder own Chart semantics. Initial resolution groups `/schemas/catalog` endpoints by the stable `resourceKey`, exposes that value as `availableResources[].id`, and keeps the catalog's governed root-relative resource path as `availableResources[].path`. Coexisting `/stats/capabilities` endpoints do not conflict with the canonical collection `/capabilities` endpoint; `chart-resource-capabilities-ambiguous` is emitted only when multiple distinct collection capability paths compete for the same resource. A selected `praxis.stats` source is normalized to that operational path before save; the discovery id is never persisted as an executable URL.
320
+ Applications should install `providePraxisCharts()` once. The registered `ComponentDocMeta.configEditor.contextResolver` materializes the editor context without making Page Builder own Chart semantics. Initial resolution groups `/schemas/catalog` endpoints by the stable `resourceKey`, exposes that value as `availableResources[].id`, and keeps the catalog's governed root-relative resource path as `availableResources[].path`. Coexisting `/stats/capabilities` endpoints do not conflict with the canonical collection `/capabilities` endpoint; `chart-resource-capabilities-endpoint-ambiguous` is emitted only when multiple distinct collection capability paths compete for the same resource. A selected `praxis.stats` source is normalized to that operational path before save; the discovery id is never persisted as an executable URL.
319
321
 
320
- Capabilities are loaded lazily from the exact catalog-published endpoint only after resource selection. Concurrent catalog and per-resource capability reads are deduplicated in flight and bounded by a 10-second timeout, but successful permission-sensitive projections are not cached across requests until the host publishes a canonical authentication/context epoch. Denied, unavailable, ambiguous, stale and empty evidence fails closed, and later requests remain retryable. In `PraxisChartWidgetConfigEditor`, authoring context diagnostics are grouped by diagnostic code with occurrence counts, scope impact, and expandable details (affected resource paths, next steps, and technical code) without dirtying the editor draft. `canonicalOperations` gates group-by, timeseries and distribution, while `stats.fields[]` gates dimensions, metrics, aggregations and exact `distributionModes` eligibility for terms versus histogram. `/schemas/filtered` enrichment of field titles, types and `x-ui` remains a later schema-flow gate; capabilities remain the authority for eligibility.
322
+ Capabilities are loaded lazily from the exact catalog-published endpoint only after resource selection. Concurrent reads are deduplicated in flight, with a 30-second catalog timeout and a 10-second per-resource capability timeout, but successful permission-sensitive projections are not cached across requests until the host publishes a canonical authentication/context epoch. Denied, unavailable, ambiguous, stale and empty evidence fails closed, and later requests remain retryable. In `PraxisChartWidgetConfigEditor`, authoring context diagnostics are grouped by diagnostic code with occurrence counts, scope impact, and expandable details (affected resource paths, next steps, and technical code) without dirtying the editor draft. `canonicalOperations` gates group-by, timeseries and distribution, while `stats.fields[]` gates dimensions, metrics, aggregations and exact `distributionModes` eligibility for terms versus histogram. `/schemas/filtered` enrichment of field titles, types and `x-ui` remains a later schema-flow gate; capabilities remain the authority for eligibility.
321
323
 
322
- When `source.kind === 'derived'`, `PraxisChartConfigEditor` and `PraxisChartWidgetConfigEditor` expose local dataset authoring (`inputs.data` / `localData`) in the Data tab, align the live preview with the widget's local records (falling back to an explicitly labeled synthetic sample only when local records are empty), and validate predictable runtime constraints before save—including single-bucket consolidation and scale range checks for `gauge`. For `pie` and `donut` charts, omitting `labels` preserves the engine's default callout visibility (`labels.enabled: undefined`), while explicit `labels: false` or `labels: { enabled: false }` hides both slice labels and leader lines (`labelLine: { show: false }`).
324
+ When `source.kind === 'derived'`, `PraxisChartConfigEditor` and `PraxisChartWidgetConfigEditor` expose local dataset authoring (`inputs.data` / `localData`) in the Data tab, align the live preview with the widget's local records (falling back to an explicitly labeled synthetic sample only when local records are empty), and validate predictable runtime constraints before save—including single-bucket consolidation and scale range checks for `gauge`. Consolidation preserves the authored aggregation: SUM40+80 remains120, and a gauge scale0..100 reports `gauge-value-out-of-range` until the author changes the scale, data or aggregation explicitly. The editor never substitutes an average or clamps the value to make it fit. For `pie` and `donut` charts, omitting `labels` preserves the engine's default callout visibility (`labels.enabled: undefined`), while explicit `labels: false` or `labels: { enabled: false }` hides both slice labels and leader lines (`labelLine: { show: false }`).
323
325
 
324
326
  Target choices come only from existing top-level composition links whose source is the current Chart and whose exact structured source event port is public and non-deprecated. Raw `pointClick` evidence never authorizes a configured action; `events.pointClick` uses `pointAction`. Directly compatible widget ports are admitted; an explicit `transform.output.semanticKind` is inspected only as provisional local evidence. That local check does not replace a transform-output projection materialized and validated by Core, which remains a P2 gate. Destinations are limited to compatible public, non-deprecated widget inputs or explicitly writable declared page state. Each target carries `events[]`, so a link from `crossFilter` cannot authorize the same target for `pointClick`, `selectionChange` or `drillDown`; routes, nested ports, global actions, undeclared state and unconnected widgets are not synthesized.
325
327
 
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "schemaVersion": "1.0.0",
3
- "generatedAt": "2026-10-15T03:52:00.118Z",
3
+ "generatedAt": "2026-10-15T04:35:19.773Z",
4
4
  "packageName": "@praxisui/charts",
5
- "packageVersion": "10.0.0-rc.20",
5
+ "packageVersion": "10.0.0-rc.21",
6
6
  "sourceRegistry": "praxis-component-registry-ingestion",
7
7
  "sourceRegistryVersion": "1.0.0",
8
8
  "componentCount": 4,
@@ -1,5 +1,5 @@
1
1
  import * as i0 from '@angular/core';
2
- import { Injectable, Optional, Inject, InjectionToken, input, booleanAttribute, output, viewChild, inject, ElementRef, NgZone, DestroyRef, signal, computed, afterNextRender, effect, ChangeDetectionStrategy, Component, ViewChild, Input, Injector, ENVIRONMENT_INITIALIZER } from '@angular/core';
2
+ import { Injectable, Optional, Inject, InjectionToken, input, booleanAttribute, output, viewChild, inject, ElementRef, NgZone, DestroyRef, signal, linkedSignal, computed, afterNextRender, effect, ChangeDetectionStrategy, Component, ViewChild, Input, Injector, ENVIRONMENT_INITIALIZER } from '@angular/core';
3
3
  import * as i1 from '@praxisui/core';
4
4
  import { buildApiUrl, API_URL, PraxisI18nService, ComponentMetadataRegistry, SETTINGS_PANEL_BRIDGE, normalizePraxisDataQueryContext, resolvePraxisFilterCriteria, BUILTIN_PAGE_THEME_PRESETS, PraxisIconButtonComponent, normalizePraxisPresentationVisualization, resolvePraxisMicroVisualizationTone, AnalyticsStatsRequestBuilderService, AnalyticsPresentationResolver, providePraxisI18n, ResourceDiscoveryService, SETTINGS_PANEL_DATA, createDefaultTableConfig, PRAXIS_TABLE_DETAIL_INLINE_RENDERERS, DynamicWidgetPageComponent } from '@praxisui/core';
5
5
  import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
@@ -2465,7 +2465,35 @@ class PraxisChartComponent {
2465
2465
  remoteResolvedData = signal(null, ...(ngDevMode ? [{ debugName: "remoteResolvedData" }] : /* istanbul ignore next */ []));
2466
2466
  remoteRuntimeState = signal('idle', ...(ngDevMode ? [{ debugName: "remoteRuntimeState" }] : /* istanbul ignore next */ []));
2467
2467
  remoteTechnicalError = signal(null, ...(ngDevMode ? [{ debugName: "remoteTechnicalError" }] : /* istanbul ignore next */ []));
2468
- runtimeChartDocument = signal(null, ...(ngDevMode ? [{ debugName: "runtimeChartDocument" }] : /* istanbul ignore next */ []));
2468
+ runtimeOwner = linkedSignal({ ...(ngDevMode ? { debugName: "runtimeOwner" } : /* istanbul ignore next */ {}), source: () => ({
2469
+ chartDocument: this.chartDocument(), config: this.config(),
2470
+ data: this.data(), queryContext: this.queryContext(),
2471
+ }),
2472
+ computation: (owner, previous) => {
2473
+ const patch = previous?.value.editorPatch;
2474
+ // An owner can acknowledge the accepted patch one input at a time. A distinct
2475
+ // external change reclaims the complete scope, including local data/context.
2476
+ if (previous && patch && Object.keys(owner).every((key) => this.editorInputSignature(owner[key]) === this.editorInputSignature(previous.source[key])
2477
+ || (Object.prototype.hasOwnProperty.call(patch, key)
2478
+ && this.editorInputSignature(owner[key]) === this.editorInputSignature(patch[key]))))
2479
+ return previous.value;
2480
+ let document = owner.chartDocument;
2481
+ if (!document && owner.config?.type && Array.isArray(owner.config.series)) {
2482
+ try {
2483
+ document = this.canonicalMapper.toPraxisXUiChartContract(owner.config);
2484
+ }
2485
+ catch {
2486
+ document = null;
2487
+ }
2488
+ }
2489
+ return {
2490
+ owner: structuredClone(owner),
2491
+ document: document ? structuredClone(document) : null,
2492
+ data: structuredClone(owner.data),
2493
+ queryContext: structuredClone(owner.queryContext),
2494
+ };
2495
+ } });
2496
+ runtimeChartDocument = computed(() => this.runtimeOwner().document, ...(ngDevMode ? [{ debugName: "runtimeChartDocument" }] : /* istanbul ignore next */ []));
2469
2497
  mappedRuntimeConfig = signal(null, ...(ngDevMode ? [{ debugName: "mappedRuntimeConfig" }] : /* istanbul ignore next */ []));
2470
2498
  chartDocumentMappingError = signal(null, ...(ngDevMode ? [{ debugName: "chartDocumentMappingError" }] : /* istanbul ignore next */ []));
2471
2499
  activeSelectionSignature = signal(null, ...(ngDevMode ? [{ debugName: "activeSelectionSignature" }] : /* istanbul ignore next */ []));
@@ -2483,13 +2511,14 @@ class PraxisChartComponent {
2483
2511
  deferredRenderAttempts = 0;
2484
2512
  previousRemoteSignature = null;
2485
2513
  previousRemoteDataResolver = undefined;
2486
- previousDocumentSignature = null;
2487
2514
  inheritedThemeSignature = null;
2488
2515
  editorSessionSubscriptions = [];
2489
2516
  remoteRequestSubscription = null;
2490
2517
  remoteRequestSequence = 0;
2491
2518
  previousSelectionScopeSignature = null;
2492
- userEditedDocument = signal(false, ...(ngDevMode ? [{ debugName: "userEditedDocument" }] : /* istanbul ignore next */ []));
2519
+ userEditedDocument = computed(() => !!this.runtimeOwner().editorPatch, ...(ngDevMode ? [{ debugName: "userEditedDocument" }] : /* istanbul ignore next */ []));
2520
+ runtimeData = computed(() => this.runtimeOwner().data, ...(ngDevMode ? [{ debugName: "runtimeData" }] : /* istanbul ignore next */ []));
2521
+ runtimeQueryContext = computed(() => this.runtimeOwner().queryContext, ...(ngDevMode ? [{ debugName: "runtimeQueryContext" }] : /* istanbul ignore next */ []));
2493
2522
  effectiveConfig = computed(() => {
2494
2523
  const directDocument = this.chartDocument();
2495
2524
  const config = this.config();
@@ -2526,7 +2555,7 @@ class PraxisChartComponent {
2526
2555
  }
2527
2556
  }
2528
2557
  const base = this.applyInheritedTheme(activeConfig, this.inheritedTheme(), this.inheritedHostTextColor());
2529
- const runtimeQueryContext = normalizePraxisDataQueryContext(this.queryContext());
2558
+ const runtimeQueryContext = normalizePraxisDataQueryContext(this.runtimeQueryContext());
2530
2559
  const runtimeFilters = resolvePraxisFilterCriteria(this.filterCriteria(), runtimeQueryContext);
2531
2560
  if (!runtimeQueryContext
2532
2561
  && !Object.keys(runtimeFilters).length) {
@@ -2538,7 +2567,7 @@ class PraxisChartComponent {
2538
2567
  return mergeRemoteQueryContext(base, runtimeQueryContext, runtimeFilters);
2539
2568
  }, ...(ngDevMode ? [{ debugName: "effectiveConfig" }] : /* istanbul ignore next */ []));
2540
2569
  resolvedData = computed(() => {
2541
- const inputData = this.data();
2570
+ const inputData = this.runtimeData();
2542
2571
  if (inputData !== null && inputData !== undefined) {
2543
2572
  return inputData;
2544
2573
  }
@@ -2616,7 +2645,7 @@ class PraxisChartComponent {
2616
2645
  : 'var(--md-sys-color-on-surface, #1a1b20)', ...(ngDevMode ? [{ debugName: "accessibleDataSurfaceTextColor" }] : /* istanbul ignore next */ []));
2617
2646
  renderConfig = computed(() => {
2618
2647
  const config = this.effectiveConfig();
2619
- const explicitData = this.data();
2648
+ const explicitData = this.runtimeData();
2620
2649
  const remoteState = this.remoteRuntimeState();
2621
2650
  const remoteData = this.remoteResolvedData();
2622
2651
  if (explicitData !== null && explicitData !== undefined) {
@@ -2689,7 +2718,7 @@ class PraxisChartComponent {
2689
2718
  }, ...(ngDevMode ? [{ debugName: "sortedAccessiblePoints" }] : /* istanbul ignore next */ []));
2690
2719
  dataDiagnostic = computed(() => {
2691
2720
  const config = this.renderConfig();
2692
- const explicitData = this.data();
2721
+ const explicitData = this.runtimeData();
2693
2722
  if (config.dataSource?.kind === 'remote'
2694
2723
  && (explicitData === null || explicitData === undefined)
2695
2724
  && this.remoteRuntimeState() !== 'ready') {
@@ -2754,7 +2783,7 @@ class PraxisChartComponent {
2754
2783
  if (config.preferredLoadState === 'error')
2755
2784
  return 'error';
2756
2785
  const dataSource = config.dataSource;
2757
- const explicitData = this.data();
2786
+ const explicitData = this.runtimeData();
2758
2787
  if (dataSource?.kind === 'remote' && (explicitData === null || explicitData === undefined)) {
2759
2788
  const remoteState = this.remoteRuntimeState();
2760
2789
  if (remoteState === 'error') {
@@ -2775,27 +2804,6 @@ class PraxisChartComponent {
2775
2804
  this.observeShellSizingContext();
2776
2805
  this.observeThemeContext();
2777
2806
  });
2778
- effect(() => {
2779
- const explicitDocument = this.chartDocument();
2780
- let document = explicitDocument;
2781
- if (!document && !this.userEditedDocument()) {
2782
- const config = this.config();
2783
- if (config && config.type && Array.isArray(config.series)) {
2784
- try {
2785
- document = this.canonicalMapper.toPraxisXUiChartContract(config);
2786
- }
2787
- catch {
2788
- document = null;
2789
- }
2790
- }
2791
- }
2792
- const signature = document ? JSON.stringify(document) : null;
2793
- if (signature === this.previousDocumentSignature) {
2794
- return;
2795
- }
2796
- this.previousDocumentSignature = signature;
2797
- this.runtimeChartDocument.set(document ? structuredClone(document) : null);
2798
- });
2799
2807
  effect(() => {
2800
2808
  const runtimeDocument = this.runtimeChartDocument();
2801
2809
  if (!runtimeDocument) {
@@ -2816,9 +2824,9 @@ class PraxisChartComponent {
2816
2824
  });
2817
2825
  effect(() => {
2818
2826
  const config = this.effectiveConfig();
2819
- const explicitData = this.data();
2827
+ const explicitData = this.runtimeData();
2820
2828
  const remoteDataResolver = this.remoteDataResolver();
2821
- const queryContext = normalizePraxisDataQueryContext(this.queryContext());
2829
+ const queryContext = normalizePraxisDataQueryContext(this.runtimeQueryContext());
2822
2830
  const nextSignature = explicitData === null || explicitData === undefined
2823
2831
  ? this.buildRemoteSignature(config, queryContext)
2824
2832
  : null;
@@ -2853,14 +2861,14 @@ class PraxisChartComponent {
2853
2861
  });
2854
2862
  effect(() => {
2855
2863
  const effectiveConfig = this.effectiveConfig();
2856
- const explicitData = this.data();
2864
+ const explicitData = this.runtimeData();
2857
2865
  if (effectiveConfig.dataSource?.kind !== 'remote' || (explicitData !== null && explicitData !== undefined)) {
2858
2866
  return;
2859
2867
  }
2860
2868
  if (this.remoteRuntimeState() === 'loading' || this.remoteResolvedData() !== null) {
2861
2869
  return;
2862
2870
  }
2863
- const queryContext = normalizePraxisDataQueryContext(this.queryContext());
2871
+ const queryContext = normalizePraxisDataQueryContext(this.runtimeQueryContext());
2864
2872
  const event = {
2865
2873
  chartId: effectiveConfig.id,
2866
2874
  dataSource: effectiveConfig.dataSource,
@@ -2996,12 +3004,13 @@ class PraxisChartComponent {
2996
3004
  return;
2997
3005
  }
2998
3006
  const focusReturnTarget = this.captureFocusReturnTarget();
3007
+ let sessionSignature = this.configEditorSessionSignature();
2999
3008
  this.configEditorContextPending.set(true);
3000
3009
  try {
3001
3010
  const resolvedContext = await this.resolveConfigEditorContext(document);
3002
- const { PraxisChartConfigEditor } = await Promise.resolve().then(function () { return praxisChartConfigEditor; });
3011
+ const { PraxisChartWidgetConfigEditor } = await Promise.resolve().then(function () { return praxisChartWidgetConfigEditor; });
3003
3012
  // Context resolution can outlive the host's customization session.
3004
- if (!this.canOpenConfigEditor())
3013
+ if (!this.canOpenConfigEditor() || this.configEditorSessionSignature() !== sessionSignature)
3005
3014
  return;
3006
3015
  this.clearEditorSessionSubscriptions();
3007
3016
  const ref = this.settingsPanel.open({
@@ -3009,27 +3018,29 @@ class PraxisChartComponent {
3009
3018
  id: `chart.${document.chartId || this.effectiveConfig().id || 'praxis-chart'}`,
3010
3019
  title: this.editChartLabel(),
3011
3020
  content: {
3012
- component: PraxisChartConfigEditor,
3021
+ component: PraxisChartWidgetConfigEditor,
3013
3022
  inputs: {
3014
- chartDocument: document,
3015
- document,
3016
- mode: 'edit',
3017
- readonly: false,
3018
- availableResources: this.resolveEditorOptions(this.availableResources(), resolvedContext['availableResources']),
3019
- availableFields: this.resolveEditorOptions(this.availableFields(), resolvedContext['availableFields']),
3020
- availableTargets: this.resolveEditorOptions(this.availableTargets(), resolvedContext['availableTargets']),
3023
+ inputs: this.configEditorInputs(document),
3024
+ context: {
3025
+ availableResources: this.resolveEditorOptions(this.availableResources(), resolvedContext['availableResources']),
3026
+ availableFields: this.resolveEditorOptions(this.availableFields(), resolvedContext['availableFields']),
3027
+ availableTargets: this.resolveEditorOptions(this.availableTargets(), resolvedContext['availableTargets']),
3028
+ },
3021
3029
  },
3022
3030
  },
3023
3031
  });
3024
- const appliedSubscription = ref.applied$
3025
- .subscribe((nextDocument) => {
3026
- if (!nextDocument) {
3032
+ const consumeEditorValue = (value, phase) => {
3033
+ if (!value?.inputs?.chartDocument || !this.canOpenConfigEditor()
3034
+ || this.configEditorSessionSignature() !== sessionSignature)
3027
3035
  return;
3028
- }
3029
- const clone = structuredClone(nextDocument);
3030
- this.userEditedDocument.set(true);
3031
- this.runtimeChartDocument.set(clone);
3032
- this.chartDocumentApplied.emit(clone);
3036
+ const nextInputs = value.inputs;
3037
+ const clone = structuredClone(nextInputs.chartDocument);
3038
+ const previousData = this.runtimeData();
3039
+ const previousQueryContext = this.runtimeQueryContext();
3040
+ const nextData = clone.source.kind !== 'praxis.stats' && Array.isArray(nextInputs.data)
3041
+ ? structuredClone(nextInputs.data)
3042
+ : null;
3043
+ const nextQueryContext = structuredClone(nextInputs.queryContext ?? null);
3033
3044
  const wasConfigDriven = !this.chartDocument();
3034
3045
  let mappedConfig;
3035
3046
  if (wasConfigDriven) {
@@ -3040,40 +3051,33 @@ class PraxisChartComponent {
3040
3051
  mappedConfig = undefined;
3041
3052
  }
3042
3053
  }
3043
- this.chartDocumentPatchChange.emit({
3044
- inputPatch: {
3045
- chartDocument: structuredClone(clone),
3046
- ...(wasConfigDriven && mappedConfig ? { config: mappedConfig } : {}),
3047
- },
3054
+ const inputPatch = {
3055
+ chartDocument: structuredClone(clone),
3056
+ ...(wasConfigDriven && mappedConfig ? { config: mappedConfig } : {}),
3057
+ ...(nextData !== null || previousData != null ? { data: structuredClone(nextData) } : {}),
3058
+ ...(nextQueryContext !== null || previousQueryContext != null
3059
+ ? { queryContext: structuredClone(nextQueryContext) } : {}),
3060
+ };
3061
+ this.runtimeOwner.set({
3062
+ owner: this.runtimeOwner().owner,
3063
+ document: clone, data: nextData, queryContext: nextQueryContext,
3064
+ // A later patch may omit config or already-cleared data/context while
3065
+ // the owner is still acknowledging those fields from the prior Apply.
3066
+ editorPatch: { ...this.runtimeOwner().editorPatch, ...structuredClone(inputPatch) },
3048
3067
  });
3049
- });
3068
+ sessionSignature = this.configEditorSessionSignature();
3069
+ if (phase === 'apply')
3070
+ this.chartDocumentApplied.emit(structuredClone(clone));
3071
+ else
3072
+ this.chartDocumentSaved.emit(structuredClone(clone));
3073
+ this.chartDocumentPatchChange.emit({ inputPatch });
3074
+ };
3075
+ const appliedSubscription = ref.applied$
3076
+ .subscribe((value) => consumeEditorValue(value, 'apply'));
3050
3077
  const savedSubscription = ref.saved$
3051
- .subscribe((nextDocument) => {
3052
- if (!nextDocument) {
3053
- return;
3054
- }
3055
- const clone = structuredClone(nextDocument);
3056
- this.userEditedDocument.set(true);
3057
- this.runtimeChartDocument.set(clone);
3058
- this.chartDocumentSaved.emit(clone);
3059
- const wasConfigDriven = !this.chartDocument();
3060
- let mappedConfig;
3061
- if (wasConfigDriven) {
3062
- try {
3063
- mappedConfig = this.canonicalMapper.toPraxisChartConfig(clone);
3064
- }
3065
- catch {
3066
- mappedConfig = undefined;
3067
- }
3068
- }
3069
- this.chartDocumentPatchChange.emit({
3070
- inputPatch: {
3071
- chartDocument: structuredClone(clone),
3072
- ...(wasConfigDriven && mappedConfig ? { config: mappedConfig } : {}),
3073
- },
3074
- });
3075
- });
3078
+ .subscribe((value) => consumeEditorValue(value, 'save'));
3076
3079
  const closedSubscription = ref.closed$?.subscribe(() => {
3080
+ this.clearEditorSessionSubscriptions();
3077
3081
  this.restoreFocus(focusReturnTarget);
3078
3082
  });
3079
3083
  this.editorSessionSubscriptions = [
@@ -3086,6 +3090,26 @@ class PraxisChartComponent {
3086
3090
  this.configEditorContextPending.set(false);
3087
3091
  }
3088
3092
  }
3093
+ configEditorSessionSignature() {
3094
+ return this.editorInputSignature(this.runtimeOwner());
3095
+ }
3096
+ editorInputSignature(value) {
3097
+ return JSON.stringify(value, (_key, current) => {
3098
+ if (!current || typeof current !== 'object' || Array.isArray(current))
3099
+ return current;
3100
+ return Object.fromEntries(Object.keys(current).sort().map((key) => [key, current[key]]));
3101
+ }) ?? '';
3102
+ }
3103
+ configEditorInputs(document) {
3104
+ const data = this.runtimeData();
3105
+ const queryContext = this.runtimeQueryContext();
3106
+ return {
3107
+ chartDocument: structuredClone(document),
3108
+ ...(!this.chartDocument() ? { config: structuredClone(this.config()) } : {}),
3109
+ ...(data != null ? { data: structuredClone(data) } : {}),
3110
+ ...(queryContext != null ? { queryContext: structuredClone(queryContext) } : {}),
3111
+ };
3112
+ }
3089
3113
  async resolveConfigEditorContext(document) {
3090
3114
  const metadata = this.componentMetadataRegistry.get('praxis-chart');
3091
3115
  const resolver = metadata?.configEditor?.contextResolver;
@@ -3097,9 +3121,7 @@ class PraxisChartComponent {
3097
3121
  componentId: metadata.id,
3098
3122
  widgetKey: document.chartId || this.effectiveConfig().id || 'praxis-chart',
3099
3123
  widgetType: metadata.id,
3100
- persistedInputs: {
3101
- chartDocument: structuredClone(document),
3102
- },
3124
+ persistedInputs: this.configEditorInputs(document),
3103
3125
  metadata,
3104
3126
  });
3105
3127
  const candidate = this.isConfigEditorContextResult(resolved)
@@ -3341,7 +3363,7 @@ class PraxisChartComponent {
3341
3363
  dataSource: config.dataSource,
3342
3364
  interactions: config.interactions,
3343
3365
  filterCriteria: this.filterCriteria(),
3344
- queryContext: normalizePraxisDataQueryContext(this.queryContext()),
3366
+ queryContext: normalizePraxisDataQueryContext(this.runtimeQueryContext()),
3345
3367
  data: this.resolvedData(),
3346
3368
  });
3347
3369
  }
@@ -3867,7 +3889,7 @@ class PraxisChartComponent {
3867
3889
  ></div>
3868
3890
  }
3869
3891
  </section>
3870
- `, isInline: true, styles: [":host{display:block;min-height:var(--praxis-chart-runtime-height, 320px);min-width:0;color:var(--md-sys-color-on-surface, #1a1b20)}:host(.praxis-chart-host-fill-container){height:100%;min-height:0}:host-context(.pdx-shell){color:inherit}:host-context(.pdx-shell.no-shell) .praxis-chart-shell,:host-context(.pdx-shell.body-fill) .praxis-chart-shell,:host-context(.pdx-shell.expanded) .praxis-chart-shell,:host-context(.pdx-shell.fullscreen) .praxis-chart-shell{height:100%!important}.praxis-chart-shell{box-sizing:border-box;position:relative;width:100%;height:100%;min-height:240px;border-radius:var(--praxis-chart-config-surface-radius, var(--praxis-chart-surface-radius, 8px));overflow:hidden;background:var( --praxis-chart-config-surface-bg, var(--praxis-chart-surface-bg, var(--md-sys-color-surface-container-lowest, #fff)) );border-color:var( --praxis-chart-config-surface-border, var( --praxis-chart-surface-border, color-mix(in srgb, var(--md-sys-color-outline, #c5c7ce) 44%, transparent) ) );border-style:solid;border-width:var(--praxis-chart-config-surface-border-width, 1px)}.praxis-chart-shell-fill-container{min-height:0}:host-context(.pdx-shell) .praxis-chart-shell:not(.praxis-chart-shell-contained),.praxis-chart-shell-embedded{border-width:var( --praxis-chart-config-surface-border-width, var(--praxis-chart-embedded-surface-border-width, 0) );border-radius:var( --praxis-chart-config-surface-radius, var(--praxis-chart-embedded-surface-radius, 0) );background:var( --praxis-chart-config-surface-bg, var(--praxis-chart-embedded-surface-bg, transparent) )}.praxis-chart-settings-trigger{position:absolute;top:10px;right:10px;z-index:3;--praxis-icon-button-filled-background: color-mix(in srgb, var(--md-sys-color-surface, #fff) 88%, rgba(18, 99, 180, .12));--praxis-icon-button-filled-foreground: var(--md-sys-color-on-surface-variant, #44474f);-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px)}.praxis-chart-data-trigger{position:absolute;top:10px;right:10px;z-index:5;--praxis-icon-button-filled-background: color-mix(in srgb, var(--md-sys-color-surface, #fff) 88%, rgba(18, 99, 180, .12));--praxis-icon-button-filled-foreground: var(--md-sys-color-on-surface-variant, #44474f);-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px)}.praxis-chart-data-trigger-with-settings{right:50px}.praxis-chart-accessible-data{position:absolute;inset:0;z-index:4;display:flex;flex-direction:column;overflow:hidden;padding:52px 16px 14px;color:var(--praxis-chart-config-text-color, var(--md-sys-color-on-surface, #1a1b20));background:var(--md-sys-color-surface-container-lowest, #fff)}.praxis-chart-accessible-data--shell-controlled{padding-top:16px}.praxis-chart-accessible-header{flex-shrink:0;margin-bottom:8px}.praxis-chart-accessible-header h3{margin:0;font-size:.95rem;font-weight:600;line-height:1.4}.praxis-chart-table-container{flex:1;min-height:0;overflow-y:auto;overflow-x:auto;overscroll-behavior:contain}.praxis-chart-accessible-data table{width:100%;border-collapse:separate;border-spacing:0;font-size:.875rem}.praxis-chart-accessible-data caption{padding:0 0 8px;text-align:left;color:inherit;opacity:.85;font-size:.8125rem}.praxis-chart-accessible-data thead{position:sticky;top:0;z-index:2}.praxis-chart-accessible-data thead th{font-weight:600;background:var(--praxis-chart-accessible-thead-bg, inherit);box-shadow:0 1px color-mix(in srgb,var(--md-sys-color-outline, #c5c7ce) 44%,transparent)}.praxis-chart-accessible-data th,.praxis-chart-accessible-data td{padding:8px;border-bottom:1px solid color-mix(in srgb,var(--md-sys-color-outline, #c5c7ce) 44%,transparent);text-align:left}.praxis-chart-col-value{text-align:right!important;font-variant-numeric:tabular-nums}.praxis-chart-sort-header{appearance:none;display:inline-flex;align-items:center;gap:4px;padding:2px 4px;border:0;border-radius:4px;background:transparent;color:inherit;font:inherit;font-weight:600;cursor:pointer;transition:background-color .15s ease}.praxis-chart-sort-header:hover{background:color-mix(in srgb,currentColor 8%,transparent)}.praxis-chart-sort-header:focus-visible{outline:2px solid currentColor;outline-offset:1px}.praxis-chart-sort-header--numeric{margin-left:auto}.praxis-chart-sort-indicator{font-size:.75rem;line-height:1;opacity:.85}.praxis-chart-point-action{appearance:none;padding:4px 6px;border:0;border-radius:4px;color:inherit;background:transparent;font:inherit;font-weight:600;text-align:left;text-decoration:none;cursor:pointer;transition:background-color .15s ease}.praxis-chart-point-action:hover{background:color-mix(in srgb,currentColor 8%,transparent)}.praxis-chart-point-action:focus-visible{outline:3px solid currentColor;outline-offset:2px}.praxis-chart-point-action[aria-pressed=true]{color:var(--md-sys-color-on-primary-container, #001b3f);background:var(--md-sys-color-primary-container, #d7e3ff)}.praxis-chart-host{width:100%;height:100%;min-height:inherit}.praxis-chart-host--data-hidden{visibility:hidden;pointer-events:none}.praxis-chart-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(--praxis-chart-config-text-color, var(--md-sys-color-on-surface, #1a1b20))}.praxis-chart-state-description{font-size:.925rem;color:color-mix(in srgb,var(--praxis-chart-config-text-color, var(--md-sys-color-on-surface-variant, #5a5d67)) 72%,transparent);max-width:36rem}.praxis-chart-loading-hero{width:min(100%,320px);display:grid;gap:14px}.praxis-chart-loading-summary{display:flex;gap:8px;justify-content:center}.praxis-chart-loading-chip,.praxis-chart-loading-bar{border-radius:999px;background:linear-gradient(90deg,#1263b414,#1263b438,#1263b414);background-size:200% 100%;animation:praxis-chart-loading-wave 1.2s ease-in-out infinite}.praxis-chart-loading-chip{display:block;width:104px;height:12px}.praxis-chart-loading-chip--short{width:64px}.praxis-chart-loading-plot{display:grid;grid-template-columns:repeat(5,minmax(0,1fr));align-items:end;gap:10px;height:108px;padding:12px 8px 4px;border-radius:8px;background:color-mix(in srgb,var(--md-sys-color-surface-container, #eef3f8) 72%,transparent);border:1px solid rgba(18,99,180,.08)}.praxis-chart-loading-bar{display:block;width:100%}.praxis-chart-loading-bar--1{height:32%}.praxis-chart-loading-bar--2{height:68%}.praxis-chart-loading-bar--3{height:48%}.praxis-chart-loading-bar--4{height:78%}.praxis-chart-loading-bar--5{height:56%}@keyframes praxis-chart-loading-wave{0%{background-position:100% 0}to{background-position:-100% 0}}\n"], dependencies: [{ kind: "ngmodule", type: MatTooltipModule }, { kind: "directive", type: i1$2.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "component", type: PraxisIconButtonComponent, selector: "button[praxisIconButton]", inputs: ["praxisIconButton", "size", "appearance", "presentation", "pressed", "busy"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
3892
+ `, isInline: true, styles: [":host{display:block;min-height:var(--praxis-chart-runtime-height, 320px);min-width:0;color:var(--md-sys-color-on-surface, #1a1b20)}:host(.praxis-chart-host-fill-container){height:100%;min-height:0}:host-context(.pdx-shell){color:inherit}:host-context(.pdx-shell.no-shell) .praxis-chart-shell,:host-context(.pdx-shell.body-fill) .praxis-chart-shell,:host-context(.pdx-shell.expanded) .praxis-chart-shell,:host-context(.pdx-shell.fullscreen) .praxis-chart-shell{height:100%!important}.praxis-chart-shell{box-sizing:border-box;position:relative;width:100%;height:100%;min-height:240px;border-radius:var(--praxis-chart-config-surface-radius, var(--praxis-chart-surface-radius, 8px));overflow:hidden;background:var( --praxis-chart-config-surface-bg, var(--praxis-chart-surface-bg, var(--md-sys-color-surface-container-lowest, #fff)) );border-color:var( --praxis-chart-config-surface-border, var( --praxis-chart-surface-border, color-mix(in srgb, var(--md-sys-color-outline, #c5c7ce) 44%, transparent) ) );border-style:solid;border-width:var(--praxis-chart-config-surface-border-width, 1px)}.praxis-chart-shell-fill-container{min-height:0}:host-context(.pdx-shell) .praxis-chart-shell:not(.praxis-chart-shell-contained),.praxis-chart-shell-embedded{border-width:var( --praxis-chart-config-surface-border-width, var(--praxis-chart-embedded-surface-border-width, 0) );border-radius:var( --praxis-chart-config-surface-radius, var(--praxis-chart-embedded-surface-radius, 0) );background:var( --praxis-chart-config-surface-bg, var(--praxis-chart-embedded-surface-bg, transparent) )}.praxis-chart-settings-trigger,.praxis-chart-data-trigger{position:absolute;top:10px;right:10px;z-index:5;--praxis-icon-button-filled-background: color-mix(in srgb, var(--md-sys-color-surface, #fff) 88%, rgba(18, 99, 180, .12));--praxis-icon-button-filled-foreground: var(--md-sys-color-on-surface-variant, #44474f);-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px)}.praxis-chart-data-trigger-with-settings{right:50px}.praxis-chart-accessible-data{position:absolute;inset:0;z-index:4;display:flex;flex-direction:column;overflow:hidden;padding:52px 16px 14px;color:var(--praxis-chart-config-text-color, var(--md-sys-color-on-surface, #1a1b20));background:var(--md-sys-color-surface-container-lowest, #fff)}.praxis-chart-accessible-data--shell-controlled{padding-top:16px}.praxis-chart-accessible-header{flex-shrink:0;margin-bottom:8px}.praxis-chart-accessible-header h3{margin:0;font-size:.95rem;font-weight:600;line-height:1.4}.praxis-chart-table-container{flex:1;min-height:0;overflow-y:auto;overflow-x:auto;overscroll-behavior:contain}.praxis-chart-accessible-data table{width:100%;border-collapse:separate;border-spacing:0;font-size:.875rem}.praxis-chart-accessible-data caption{padding:0 0 8px;text-align:left;color:inherit;opacity:.85;font-size:.8125rem}.praxis-chart-accessible-data thead{position:sticky;top:0;z-index:2}.praxis-chart-accessible-data thead th{font-weight:600;background:var(--praxis-chart-accessible-thead-bg, inherit);box-shadow:0 1px color-mix(in srgb,var(--md-sys-color-outline, #c5c7ce) 44%,transparent)}.praxis-chart-accessible-data th,.praxis-chart-accessible-data td{padding:8px;border-bottom:1px solid color-mix(in srgb,var(--md-sys-color-outline, #c5c7ce) 44%,transparent);text-align:left}.praxis-chart-col-value{text-align:right!important;font-variant-numeric:tabular-nums}.praxis-chart-sort-header{appearance:none;display:inline-flex;align-items:center;gap:4px;padding:2px 4px;border:0;border-radius:4px;background:transparent;color:inherit;font:inherit;font-weight:600;cursor:pointer;transition:background-color .15s ease}.praxis-chart-sort-header:hover{background:color-mix(in srgb,currentColor 8%,transparent)}.praxis-chart-sort-header:focus-visible{outline:2px solid currentColor;outline-offset:1px}.praxis-chart-sort-header--numeric{margin-left:auto}.praxis-chart-sort-indicator{font-size:.75rem;line-height:1;opacity:.85}.praxis-chart-point-action{appearance:none;padding:4px 6px;border:0;border-radius:4px;color:inherit;background:transparent;font:inherit;font-weight:600;text-align:left;text-decoration:none;cursor:pointer;transition:background-color .15s ease}.praxis-chart-point-action:hover{background:color-mix(in srgb,currentColor 8%,transparent)}.praxis-chart-point-action:focus-visible{outline:3px solid currentColor;outline-offset:2px}.praxis-chart-point-action[aria-pressed=true]{color:var(--md-sys-color-on-primary-container, #001b3f);background:var(--md-sys-color-primary-container, #d7e3ff)}.praxis-chart-host{width:100%;height:100%;min-height:inherit}.praxis-chart-host--data-hidden{visibility:hidden;pointer-events:none}.praxis-chart-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(--praxis-chart-config-text-color, var(--md-sys-color-on-surface, #1a1b20))}.praxis-chart-state-description{font-size:.925rem;color:color-mix(in srgb,var(--praxis-chart-config-text-color, var(--md-sys-color-on-surface-variant, #5a5d67)) 72%,transparent);max-width:36rem}.praxis-chart-loading-hero{width:min(100%,320px);display:grid;gap:14px}.praxis-chart-loading-summary{display:flex;gap:8px;justify-content:center}.praxis-chart-loading-chip,.praxis-chart-loading-bar{border-radius:999px;background:linear-gradient(90deg,#1263b414,#1263b438,#1263b414);background-size:200% 100%;animation:praxis-chart-loading-wave 1.2s ease-in-out infinite}.praxis-chart-loading-chip{display:block;width:104px;height:12px}.praxis-chart-loading-chip--short{width:64px}.praxis-chart-loading-plot{display:grid;grid-template-columns:repeat(5,minmax(0,1fr));align-items:end;gap:10px;height:108px;padding:12px 8px 4px;border-radius:8px;background:color-mix(in srgb,var(--md-sys-color-surface-container, #eef3f8) 72%,transparent);border:1px solid rgba(18,99,180,.08)}.praxis-chart-loading-bar{display:block;width:100%}.praxis-chart-loading-bar--1{height:32%}.praxis-chart-loading-bar--2{height:68%}.praxis-chart-loading-bar--3{height:48%}.praxis-chart-loading-bar--4{height:78%}.praxis-chart-loading-bar--5{height:56%}@keyframes praxis-chart-loading-wave{0%{background-position:100% 0}to{background-position:-100% 0}}\n"], dependencies: [{ kind: "ngmodule", type: MatTooltipModule }, { kind: "directive", type: i1$2.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "component", type: PraxisIconButtonComponent, selector: "button[praxisIconButton]", inputs: ["praxisIconButton", "size", "appearance", "presentation", "pressed", "busy"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
3871
3893
  }
3872
3894
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: PraxisChartComponent, decorators: [{
3873
3895
  type: Component,
@@ -4072,7 +4094,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
4072
4094
  ></div>
4073
4095
  }
4074
4096
  </section>
4075
- `, styles: [":host{display:block;min-height:var(--praxis-chart-runtime-height, 320px);min-width:0;color:var(--md-sys-color-on-surface, #1a1b20)}:host(.praxis-chart-host-fill-container){height:100%;min-height:0}:host-context(.pdx-shell){color:inherit}:host-context(.pdx-shell.no-shell) .praxis-chart-shell,:host-context(.pdx-shell.body-fill) .praxis-chart-shell,:host-context(.pdx-shell.expanded) .praxis-chart-shell,:host-context(.pdx-shell.fullscreen) .praxis-chart-shell{height:100%!important}.praxis-chart-shell{box-sizing:border-box;position:relative;width:100%;height:100%;min-height:240px;border-radius:var(--praxis-chart-config-surface-radius, var(--praxis-chart-surface-radius, 8px));overflow:hidden;background:var( --praxis-chart-config-surface-bg, var(--praxis-chart-surface-bg, var(--md-sys-color-surface-container-lowest, #fff)) );border-color:var( --praxis-chart-config-surface-border, var( --praxis-chart-surface-border, color-mix(in srgb, var(--md-sys-color-outline, #c5c7ce) 44%, transparent) ) );border-style:solid;border-width:var(--praxis-chart-config-surface-border-width, 1px)}.praxis-chart-shell-fill-container{min-height:0}:host-context(.pdx-shell) .praxis-chart-shell:not(.praxis-chart-shell-contained),.praxis-chart-shell-embedded{border-width:var( --praxis-chart-config-surface-border-width, var(--praxis-chart-embedded-surface-border-width, 0) );border-radius:var( --praxis-chart-config-surface-radius, var(--praxis-chart-embedded-surface-radius, 0) );background:var( --praxis-chart-config-surface-bg, var(--praxis-chart-embedded-surface-bg, transparent) )}.praxis-chart-settings-trigger{position:absolute;top:10px;right:10px;z-index:3;--praxis-icon-button-filled-background: color-mix(in srgb, var(--md-sys-color-surface, #fff) 88%, rgba(18, 99, 180, .12));--praxis-icon-button-filled-foreground: var(--md-sys-color-on-surface-variant, #44474f);-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px)}.praxis-chart-data-trigger{position:absolute;top:10px;right:10px;z-index:5;--praxis-icon-button-filled-background: color-mix(in srgb, var(--md-sys-color-surface, #fff) 88%, rgba(18, 99, 180, .12));--praxis-icon-button-filled-foreground: var(--md-sys-color-on-surface-variant, #44474f);-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px)}.praxis-chart-data-trigger-with-settings{right:50px}.praxis-chart-accessible-data{position:absolute;inset:0;z-index:4;display:flex;flex-direction:column;overflow:hidden;padding:52px 16px 14px;color:var(--praxis-chart-config-text-color, var(--md-sys-color-on-surface, #1a1b20));background:var(--md-sys-color-surface-container-lowest, #fff)}.praxis-chart-accessible-data--shell-controlled{padding-top:16px}.praxis-chart-accessible-header{flex-shrink:0;margin-bottom:8px}.praxis-chart-accessible-header h3{margin:0;font-size:.95rem;font-weight:600;line-height:1.4}.praxis-chart-table-container{flex:1;min-height:0;overflow-y:auto;overflow-x:auto;overscroll-behavior:contain}.praxis-chart-accessible-data table{width:100%;border-collapse:separate;border-spacing:0;font-size:.875rem}.praxis-chart-accessible-data caption{padding:0 0 8px;text-align:left;color:inherit;opacity:.85;font-size:.8125rem}.praxis-chart-accessible-data thead{position:sticky;top:0;z-index:2}.praxis-chart-accessible-data thead th{font-weight:600;background:var(--praxis-chart-accessible-thead-bg, inherit);box-shadow:0 1px color-mix(in srgb,var(--md-sys-color-outline, #c5c7ce) 44%,transparent)}.praxis-chart-accessible-data th,.praxis-chart-accessible-data td{padding:8px;border-bottom:1px solid color-mix(in srgb,var(--md-sys-color-outline, #c5c7ce) 44%,transparent);text-align:left}.praxis-chart-col-value{text-align:right!important;font-variant-numeric:tabular-nums}.praxis-chart-sort-header{appearance:none;display:inline-flex;align-items:center;gap:4px;padding:2px 4px;border:0;border-radius:4px;background:transparent;color:inherit;font:inherit;font-weight:600;cursor:pointer;transition:background-color .15s ease}.praxis-chart-sort-header:hover{background:color-mix(in srgb,currentColor 8%,transparent)}.praxis-chart-sort-header:focus-visible{outline:2px solid currentColor;outline-offset:1px}.praxis-chart-sort-header--numeric{margin-left:auto}.praxis-chart-sort-indicator{font-size:.75rem;line-height:1;opacity:.85}.praxis-chart-point-action{appearance:none;padding:4px 6px;border:0;border-radius:4px;color:inherit;background:transparent;font:inherit;font-weight:600;text-align:left;text-decoration:none;cursor:pointer;transition:background-color .15s ease}.praxis-chart-point-action:hover{background:color-mix(in srgb,currentColor 8%,transparent)}.praxis-chart-point-action:focus-visible{outline:3px solid currentColor;outline-offset:2px}.praxis-chart-point-action[aria-pressed=true]{color:var(--md-sys-color-on-primary-container, #001b3f);background:var(--md-sys-color-primary-container, #d7e3ff)}.praxis-chart-host{width:100%;height:100%;min-height:inherit}.praxis-chart-host--data-hidden{visibility:hidden;pointer-events:none}.praxis-chart-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(--praxis-chart-config-text-color, var(--md-sys-color-on-surface, #1a1b20))}.praxis-chart-state-description{font-size:.925rem;color:color-mix(in srgb,var(--praxis-chart-config-text-color, var(--md-sys-color-on-surface-variant, #5a5d67)) 72%,transparent);max-width:36rem}.praxis-chart-loading-hero{width:min(100%,320px);display:grid;gap:14px}.praxis-chart-loading-summary{display:flex;gap:8px;justify-content:center}.praxis-chart-loading-chip,.praxis-chart-loading-bar{border-radius:999px;background:linear-gradient(90deg,#1263b414,#1263b438,#1263b414);background-size:200% 100%;animation:praxis-chart-loading-wave 1.2s ease-in-out infinite}.praxis-chart-loading-chip{display:block;width:104px;height:12px}.praxis-chart-loading-chip--short{width:64px}.praxis-chart-loading-plot{display:grid;grid-template-columns:repeat(5,minmax(0,1fr));align-items:end;gap:10px;height:108px;padding:12px 8px 4px;border-radius:8px;background:color-mix(in srgb,var(--md-sys-color-surface-container, #eef3f8) 72%,transparent);border:1px solid rgba(18,99,180,.08)}.praxis-chart-loading-bar{display:block;width:100%}.praxis-chart-loading-bar--1{height:32%}.praxis-chart-loading-bar--2{height:68%}.praxis-chart-loading-bar--3{height:48%}.praxis-chart-loading-bar--4{height:78%}.praxis-chart-loading-bar--5{height:56%}@keyframes praxis-chart-loading-wave{0%{background-position:100% 0}to{background-position:-100% 0}}\n"] }]
4097
+ `, styles: [":host{display:block;min-height:var(--praxis-chart-runtime-height, 320px);min-width:0;color:var(--md-sys-color-on-surface, #1a1b20)}:host(.praxis-chart-host-fill-container){height:100%;min-height:0}:host-context(.pdx-shell){color:inherit}:host-context(.pdx-shell.no-shell) .praxis-chart-shell,:host-context(.pdx-shell.body-fill) .praxis-chart-shell,:host-context(.pdx-shell.expanded) .praxis-chart-shell,:host-context(.pdx-shell.fullscreen) .praxis-chart-shell{height:100%!important}.praxis-chart-shell{box-sizing:border-box;position:relative;width:100%;height:100%;min-height:240px;border-radius:var(--praxis-chart-config-surface-radius, var(--praxis-chart-surface-radius, 8px));overflow:hidden;background:var( --praxis-chart-config-surface-bg, var(--praxis-chart-surface-bg, var(--md-sys-color-surface-container-lowest, #fff)) );border-color:var( --praxis-chart-config-surface-border, var( --praxis-chart-surface-border, color-mix(in srgb, var(--md-sys-color-outline, #c5c7ce) 44%, transparent) ) );border-style:solid;border-width:var(--praxis-chart-config-surface-border-width, 1px)}.praxis-chart-shell-fill-container{min-height:0}:host-context(.pdx-shell) .praxis-chart-shell:not(.praxis-chart-shell-contained),.praxis-chart-shell-embedded{border-width:var( --praxis-chart-config-surface-border-width, var(--praxis-chart-embedded-surface-border-width, 0) );border-radius:var( --praxis-chart-config-surface-radius, var(--praxis-chart-embedded-surface-radius, 0) );background:var( --praxis-chart-config-surface-bg, var(--praxis-chart-embedded-surface-bg, transparent) )}.praxis-chart-settings-trigger,.praxis-chart-data-trigger{position:absolute;top:10px;right:10px;z-index:5;--praxis-icon-button-filled-background: color-mix(in srgb, var(--md-sys-color-surface, #fff) 88%, rgba(18, 99, 180, .12));--praxis-icon-button-filled-foreground: var(--md-sys-color-on-surface-variant, #44474f);-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px)}.praxis-chart-data-trigger-with-settings{right:50px}.praxis-chart-accessible-data{position:absolute;inset:0;z-index:4;display:flex;flex-direction:column;overflow:hidden;padding:52px 16px 14px;color:var(--praxis-chart-config-text-color, var(--md-sys-color-on-surface, #1a1b20));background:var(--md-sys-color-surface-container-lowest, #fff)}.praxis-chart-accessible-data--shell-controlled{padding-top:16px}.praxis-chart-accessible-header{flex-shrink:0;margin-bottom:8px}.praxis-chart-accessible-header h3{margin:0;font-size:.95rem;font-weight:600;line-height:1.4}.praxis-chart-table-container{flex:1;min-height:0;overflow-y:auto;overflow-x:auto;overscroll-behavior:contain}.praxis-chart-accessible-data table{width:100%;border-collapse:separate;border-spacing:0;font-size:.875rem}.praxis-chart-accessible-data caption{padding:0 0 8px;text-align:left;color:inherit;opacity:.85;font-size:.8125rem}.praxis-chart-accessible-data thead{position:sticky;top:0;z-index:2}.praxis-chart-accessible-data thead th{font-weight:600;background:var(--praxis-chart-accessible-thead-bg, inherit);box-shadow:0 1px color-mix(in srgb,var(--md-sys-color-outline, #c5c7ce) 44%,transparent)}.praxis-chart-accessible-data th,.praxis-chart-accessible-data td{padding:8px;border-bottom:1px solid color-mix(in srgb,var(--md-sys-color-outline, #c5c7ce) 44%,transparent);text-align:left}.praxis-chart-col-value{text-align:right!important;font-variant-numeric:tabular-nums}.praxis-chart-sort-header{appearance:none;display:inline-flex;align-items:center;gap:4px;padding:2px 4px;border:0;border-radius:4px;background:transparent;color:inherit;font:inherit;font-weight:600;cursor:pointer;transition:background-color .15s ease}.praxis-chart-sort-header:hover{background:color-mix(in srgb,currentColor 8%,transparent)}.praxis-chart-sort-header:focus-visible{outline:2px solid currentColor;outline-offset:1px}.praxis-chart-sort-header--numeric{margin-left:auto}.praxis-chart-sort-indicator{font-size:.75rem;line-height:1;opacity:.85}.praxis-chart-point-action{appearance:none;padding:4px 6px;border:0;border-radius:4px;color:inherit;background:transparent;font:inherit;font-weight:600;text-align:left;text-decoration:none;cursor:pointer;transition:background-color .15s ease}.praxis-chart-point-action:hover{background:color-mix(in srgb,currentColor 8%,transparent)}.praxis-chart-point-action:focus-visible{outline:3px solid currentColor;outline-offset:2px}.praxis-chart-point-action[aria-pressed=true]{color:var(--md-sys-color-on-primary-container, #001b3f);background:var(--md-sys-color-primary-container, #d7e3ff)}.praxis-chart-host{width:100%;height:100%;min-height:inherit}.praxis-chart-host--data-hidden{visibility:hidden;pointer-events:none}.praxis-chart-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(--praxis-chart-config-text-color, var(--md-sys-color-on-surface, #1a1b20))}.praxis-chart-state-description{font-size:.925rem;color:color-mix(in srgb,var(--praxis-chart-config-text-color, var(--md-sys-color-on-surface-variant, #5a5d67)) 72%,transparent);max-width:36rem}.praxis-chart-loading-hero{width:min(100%,320px);display:grid;gap:14px}.praxis-chart-loading-summary{display:flex;gap:8px;justify-content:center}.praxis-chart-loading-chip,.praxis-chart-loading-bar{border-radius:999px;background:linear-gradient(90deg,#1263b414,#1263b438,#1263b414);background-size:200% 100%;animation:praxis-chart-loading-wave 1.2s ease-in-out infinite}.praxis-chart-loading-chip{display:block;width:104px;height:12px}.praxis-chart-loading-chip--short{width:64px}.praxis-chart-loading-plot{display:grid;grid-template-columns:repeat(5,minmax(0,1fr));align-items:end;gap:10px;height:108px;padding:12px 8px 4px;border-radius:8px;background:color-mix(in srgb,var(--md-sys-color-surface-container, #eef3f8) 72%,transparent);border:1px solid rgba(18,99,180,.08)}.praxis-chart-loading-bar{display:block;width:100%}.praxis-chart-loading-bar--1{height:32%}.praxis-chart-loading-bar--2{height:68%}.praxis-chart-loading-bar--3{height:48%}.praxis-chart-loading-bar--4{height:78%}.praxis-chart-loading-bar--5{height:56%}@keyframes praxis-chart-loading-wave{0%{background-position:100% 0}to{background-position:-100% 0}}\n"] }]
4076
4098
  }], ctorParameters: () => [], propDecorators: { config: [{ type: i0.Input, args: [{ isSignal: true, alias: "config", required: false }] }], resourceKey: [{ type: i0.Input, args: [{ isSignal: true, alias: "resourceKey", required: false }] }], data: [{ type: i0.Input, args: [{ isSignal: true, alias: "data", required: false }] }], chartDocument: [{ type: i0.Input, args: [{ isSignal: true, alias: "chartDocument", required: false }] }], filterCriteria: [{ type: i0.Input, args: [{ isSignal: true, alias: "filterCriteria", required: false }] }], queryContext: [{ type: i0.Input, args: [{ isSignal: true, alias: "queryContext", required: false }] }], remoteDataResolver: [{ type: i0.Input, args: [{ isSignal: true, alias: "remoteDataResolver", required: false }] }], enableCustomization: [{ type: i0.Input, args: [{ isSignal: true, alias: "enableCustomization", required: false }] }], availableResources: [{ type: i0.Input, args: [{ isSignal: true, alias: "availableResources", required: false }] }], availableFields: [{ type: i0.Input, args: [{ isSignal: true, alias: "availableFields", required: false }] }], availableTargets: [{ type: i0.Input, args: [{ isSignal: true, alias: "availableTargets", required: false }] }], pointClick: [{ type: i0.Output, args: ["pointClick"] }], pointAction: [{ type: i0.Output, args: ["pointAction"] }], selectionChange: [{ type: i0.Output, args: ["selectionChange"] }], drillDown: [{ type: i0.Output, args: ["drillDown"] }], crossFilter: [{ type: i0.Output, args: ["crossFilter"] }], queryRequest: [{ type: i0.Output, args: ["queryRequest"] }], loadStateChange: [{ type: i0.Output, args: ["loadStateChange"] }], chartDocumentApplied: [{ type: i0.Output, args: ["chartDocumentApplied"] }], chartDocumentSaved: [{ type: i0.Output, args: ["chartDocumentSaved"] }], chartDocumentPatchChange: [{ type: i0.Output, args: ["chartDocumentPatchChange"] }], chartHost: [{ type: i0.ViewChild, args: ['chartHost', { isSignal: true }] }] } });
4077
4099
  function mergeRemoteQueryContext(config, queryContext, runtimeFilters) {
4078
4100
  const dataSource = config.dataSource;
@@ -9932,14 +9954,6 @@ class PraxisChartConfigEditor {
9932
9954
  consolidatedValue = avgValue;
9933
9955
  break;
9934
9956
  }
9935
- const scale = this.normalizedDocument().gauge?.scale;
9936
- if (scale && Number.isFinite(scale.min) && Number.isFinite(scale.max) && scale.max > scale.min) {
9937
- if (consolidatedValue < scale.min || consolidatedValue > scale.max) {
9938
- consolidatedValue = avgValue >= scale.min && avgValue <= scale.max
9939
- ? avgValue
9940
- : Math.min(scale.max, Math.max(scale.min, consolidatedValue));
9941
- }
9942
- }
9943
9957
  const consolidatedRow = {
9944
9958
  ...(rows[0] ? structuredClone(rows[0]) : {}),
9945
9959
  [dimField]: String(rows[0]?.[dimField] ?? 'Total'),
@@ -10444,11 +10458,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
10444
10458
  ], providers: [providePraxisChartsI18n()], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"editor-shell\">\n <div class=\"editor-nav\">\n @for (section of sections; track section.id) {\n <button\n mat-stroked-button\n type=\"button\"\n [attr.data-testid]=\"'chart-editor-section-' + section.id\"\n [class.active]=\"activeSection() === section.id\"\n (click)=\"setSection(section.id)\"\n >\n {{ t(section.labelKey, section.fallback) }}\n </button>\n }\n </div>\n\n <div class=\"editor-layout\">\n <div class=\"editor-form\">\n <mat-card class=\"editor-card\">\n <mat-card-content>\n @switch (activeSection()) {\n @case ('general') {\n <div class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.chartId', 'Chart ID') }}</mat-label>\n <input matInput [ngModel]=\"doc().chartId || ''\" (ngModelChange)=\"setChartId($event)\" [disabled]=\"isReadonly()\" />\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.kind', 'Kind') }}</mat-label>\n <mat-select [ngModel]=\"doc().kind\" (ngModelChange)=\"setKind($event)\" [disabled]=\"isReadonly()\">\n @for (kind of chartKinds; track kind) {\n <mat-option [value]=\"kind\">\n {{ t('praxis.charts.editor.kind.' + kind, kind) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.title', 'Title') }}</mat-label>\n <input matInput data-testid=\"chart-editor-title-input\" [ngModel]=\"titleValue()\" (ngModelChange)=\"setTitle($event)\" [disabled]=\"isReadonly()\" />\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.subtitle', 'Subtitle') }}</mat-label>\n <input matInput [ngModel]=\"subtitleValue()\" (ngModelChange)=\"setSubtitle($event)\" [disabled]=\"isReadonly()\" />\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.sizingMode', 'Sizing') }}</mat-label>\n <mat-select [ngModel]=\"sizingModeValue()\" (ngModelChange)=\"setSizingMode($event)\" [disabled]=\"isReadonly()\">\n @for (mode of sizingModes; track mode) {\n <mat-option [value]=\"mode\">\n {{ t('praxis.charts.editor.sizing.' + mode, mode) }}\n </mat-option>\n }\n </mat-select>\n <mat-hint>{{ t('praxis.charts.editor.hint.sizingMode', 'Use fill-container only when the host widget provides a defined body height.') }}</mat-hint>\n </mat-form-field>\n\n @if (sizingModeValue() === 'fixed') {\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.height', 'Height') }}</mat-label>\n <input matInput [ngModel]=\"heightValue()\" (ngModelChange)=\"setSizingHeight($event)\" [disabled]=\"isReadonly()\" />\n <mat-hint>{{ t('praxis.charts.editor.hint.height', 'Numbers are saved as pixels; CSS lengths such as 20rem are also accepted.') }}</mat-hint>\n </mat-form-field>\n }\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.minHeight', 'Minimum height') }}</mat-label>\n <input matInput [ngModel]=\"sizingMinHeightValue()\" (ngModelChange)=\"setSizingMinHeight($event)\" [disabled]=\"isReadonly()\" />\n <mat-hint>{{ t('praxis.charts.editor.hint.minHeight', 'Set a readable minimum for compact dashboard widgets.') }}</mat-hint>\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.maxHeight', 'Maximum height') }}</mat-label>\n <input matInput [ngModel]=\"sizingMaxHeightValue()\" (ngModelChange)=\"setSizingMaxHeight($event)\" [disabled]=\"isReadonly()\" />\n <mat-hint>{{ t('praxis.charts.editor.hint.maxHeight', 'Leave empty unless the chart must stop growing inside a flexible layout.') }}</mat-hint>\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.aspectRatio', 'Aspect ratio') }}</mat-label>\n <input matInput [ngModel]=\"sizingAspectRatioValue()\" (ngModelChange)=\"setSizingAspectRatio($event)\" [disabled]=\"isReadonly()\" />\n <mat-hint>{{ t('praxis.charts.editor.hint.aspectRatio', 'Optional. Use values such as 1.777 or 16 / 9.') }}</mat-hint>\n </mat-form-field>\n </div>\n }\n\n @case ('data') {\n <div class=\"editor-stack\">\n <div class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.sourceKind', 'Source') }}</mat-label>\n <mat-select data-testid=\"chart-editor-source-kind\" [ngModel]=\"doc().source.kind\" (ngModelChange)=\"setSourceKind($event)\" [disabled]=\"isReadonly()\">\n @for (sourceKind of sourceKinds; track sourceKind) {\n <mat-option [value]=\"sourceKind\">\n {{ t('praxis.charts.editor.sourceKind.' + (sourceKind === 'praxis.stats' ? 'praxisStats' : 'derived'), sourceKind) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n @if (doc().source.kind === 'praxis.stats') {\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.resource', 'Resource') }}</mat-label>\n <mat-select data-testid=\"chart-editor-resource\" [ngModel]=\"resourceValue()\" (ngModelChange)=\"setResource($event)\" [disabled]=\"isReadonly() || resourceCatalogUnavailable()\">\n @for (resource of resourceOptions(); track resource.id) {\n <mat-option [value]=\"resource.path\">{{ resource.label }}</mat-option>\n }\n </mat-select>\n @if (resourceCatalogUnavailable()) {\n <mat-hint>{{ t('praxis.charts.editor.catalog.resourceMissing', 'Resource catalog is required for governed praxis.stats authoring.') }}</mat-hint>\n }\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.operation', 'Operation') }}</mat-label>\n <mat-select\n data-testid=\"chart-editor-operation\"\n [ngModel]=\"doc().source.operation || 'group-by'\"\n (ngModelChange)=\"setOperation($event)\"\n [disabled]=\"isReadonly() || operationCatalogUnavailable()\"\n >\n @for (operation of operationOptions(); track operation) {\n <mat-option [value]=\"operation\">\n {{ t('praxis.charts.editor.operation.' + (operation === 'group-by' ? 'groupBy' : operation), operation) }}\n </mat-option>\n }\n </mat-select>\n @if (operationCatalogUnavailable()) {\n <mat-hint>{{ t('praxis.charts.editor.catalog.operationMissing', 'Selected resource does not publish authorable stats operations.') }}</mat-hint>\n }\n </mat-form-field>\n }\n </div>\n\n @if (showTimeseriesControls()) {\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.specialization.timeseriesTitle', 'Timeseries options') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.granularity', 'Granularity') }}</mat-label>\n <mat-select [ngModel]=\"granularityValue()\" (ngModelChange)=\"setGranularity($event)\" [disabled]=\"isReadonly()\">\n @for (granularity of timeGranularities; track granularity) {\n <mat-option [value]=\"granularity\">\n {{ t('praxis.charts.editor.granularity.' + granularity, granularity) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n <mat-slide-toggle\n [ngModel]=\"fillGapsValue()\"\n (ngModelChange)=\"setFillGaps($event)\"\n [disabled]=\"isReadonly()\"\n >\n {{ t('praxis.charts.editor.field.fillGaps', 'Fill missing intervals') }}\n </mat-slide-toggle>\n </mat-card-content>\n </mat-card>\n }\n\n @if (showDistributionControls()) {\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.specialization.distributionTitle', 'Distribution options') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.distributionMode', 'Distribution mode') }}</mat-label>\n <mat-select data-testid=\"chart-editor-distribution-mode\" [ngModel]=\"distributionModeValue()\" (ngModelChange)=\"setDistributionMode($event)\" [disabled]=\"isReadonly()\">\n @for (mode of distributionModes; track mode) {\n <mat-option [value]=\"mode\">\n {{ t('praxis.charts.editor.distributionMode.' + mode, mode) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n @if (distributionModeValue() === 'histogram') {\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.bucketSize', 'Bucket size') }}</mat-label>\n <input matInput data-testid=\"chart-editor-bucket-size\" [ngModel]=\"bucketSizeValue()\" (ngModelChange)=\"setBucketSize($event)\" [disabled]=\"isReadonly()\" />\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.bucketCount', 'Bucket count') }}</mat-label>\n <input matInput data-testid=\"chart-editor-bucket-count\" [ngModel]=\"bucketCountValue()\" (ngModelChange)=\"setBucketCount($event)\" [disabled]=\"isReadonly()\" />\n </mat-form-field>\n }\n </mat-card-content>\n </mat-card>\n }\n\n @if (showComparisonControls()) {\n <mat-card class=\"editor-card\" data-testid=\"chart-editor-comparison-options\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.specialization.comparisonTitle', 'Comparison period') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.comparisonPeriodField', 'Period field') }}</mat-label>\n @if (fieldOptions('time').length) {\n <mat-select\n [ngModel]=\"comparisonPeriodFieldValue()\"\n (ngModelChange)=\"setComparisonPeriodField($event)\"\n [disabled]=\"isReadonly()\"\n >\n @for (field of fieldOptions('time'); track field.field) {\n <mat-option [value]=\"field.field\">{{ field.label || field.field }}</mat-option>\n }\n </mat-select>\n } @else {\n <mat-select [ngModel]=\"comparisonPeriodFieldValue()\" [disabled]=\"true\"></mat-select>\n <mat-hint>{{ t('praxis.charts.editor.catalog.comparisonPeriodFieldsMissing', 'No governed time-series field is available for comparison periods.') }}</mat-hint>\n }\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.comparisonTimezone', 'Timezone') }}</mat-label>\n <input\n matInput\n [ngModel]=\"comparisonPeriodTimezoneValue()\"\n (ngModelChange)=\"setComparisonPeriodTimezone($event)\"\n [disabled]=\"isReadonly()\"\n />\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.comparisonPreset', 'Period preset') }}</mat-label>\n <mat-select\n [ngModel]=\"comparisonPeriodPresetValue()\"\n (ngModelChange)=\"setComparisonPeriodPreset($event)\"\n [disabled]=\"isReadonly()\"\n >\n @for (preset of comparisonPeriodPresets; track preset) {\n <mat-option [value]=\"preset\">\n {{ t('praxis.charts.editor.comparisonPreset.' + preset, preset) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.comparisonMode', 'Comparison mode') }}</mat-label>\n <mat-select\n [ngModel]=\"comparisonPeriodModeValue()\"\n (ngModelChange)=\"setComparisonPeriodMode($event)\"\n [disabled]=\"isReadonly()\"\n >\n @for (mode of comparisonPeriodModes; track mode) {\n <mat-option [value]=\"mode\">\n {{ t('praxis.charts.editor.comparisonMode.' + mode, mode) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n </mat-card-content>\n </mat-card>\n }\n\n @if (doc().source.kind === 'derived') {\n <mat-card class=\"editor-card\" data-testid=\"chart-editor-local-data\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.localData.title', 'Local dataset (inputs.data)') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-stack\">\n <p class=\"editor-caption\">\n {{ t('praxis.charts.editor.localData.caption', 'Edit the local records rendered when the data source is derived.') }}\n </p>\n\n @if (canConsolidateGaugeLocalData()) {\n <div class=\"editor-row-actions\">\n <button\n mat-stroked-button\n type=\"button\"\n data-testid=\"chart-local-data-consolidate-gauge\"\n (click)=\"consolidateGaugeLocalData()\"\n [disabled]=\"isReadonly()\"\n >\n {{ t('praxis.charts.editor.localData.consolidateGauge', 'Consolidate to 1 bucket for gauge') }}\n </button>\n </div>\n }\n\n @if (localDataRows().length) {\n @for (row of localDataRows(); track $index; let rowIndex = $index) {\n <div class=\"editor-row-card\" [attr.data-testid]=\"'chart-local-data-row-' + rowIndex\">\n <div class=\"editor-grid\">\n @for (dimField of localDataDimensionFields(); track dimField) {\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ dimField }}</mat-label>\n <input\n matInput\n [attr.data-testid]=\"'chart-local-data-dim-' + rowIndex + '-' + dimField\"\n [ngModel]=\"localDataCellStringValue(rowIndex, dimField)\"\n (ngModelChange)=\"setLocalDataCellValue(rowIndex, dimField, $event, 'dimension')\"\n [disabled]=\"isReadonly()\"\n />\n </mat-form-field>\n }\n @for (metricField of localDataMetricFields(); track metricField) {\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ metricField }}</mat-label>\n <input\n matInput\n type=\"number\"\n [attr.data-testid]=\"'chart-local-data-metric-' + rowIndex + '-' + metricField\"\n [ngModel]=\"localDataCellStringValue(rowIndex, metricField)\"\n (ngModelChange)=\"setLocalDataCellValue(rowIndex, metricField, $event, 'metric')\"\n [disabled]=\"isReadonly()\"\n />\n </mat-form-field>\n }\n </div>\n <div class=\"editor-row-actions\">\n <button\n mat-button\n type=\"button\"\n [attr.data-testid]=\"'chart-local-data-remove-row-' + rowIndex\"\n (click)=\"removeLocalDataRow(rowIndex)\"\n [disabled]=\"isReadonly()\"\n >\n {{ t('praxis.charts.editor.localData.removeRow', 'Remove record') }}\n </button>\n </div>\n </div>\n }\n } @else {\n <div class=\"editor-empty\" data-testid=\"chart-local-data-empty\">\n {{ t('praxis.charts.editor.localData.empty', 'No local records configured yet. Add a record to replace the synthetic preview sample.') }}\n </div>\n }\n\n <div>\n <button\n mat-stroked-button\n type=\"button\"\n data-testid=\"chart-local-data-add-row\"\n (click)=\"addLocalDataRow()\"\n [disabled]=\"isReadonly()\"\n >\n {{ t('praxis.charts.editor.localData.addRow', 'Add record') }}\n </button>\n </div>\n </mat-card-content>\n </mat-card>\n }\n </div>\n }\n\n @case ('motion') {\n <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 (showGaugePanel()) {\n <mat-card class=\"editor-card\" data-testid=\"chart-editor-gauge-scale\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.specialization.gaugeTitle', 'Gauge scale') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-stack\">\n <p class=\"editor-caption\">\n {{ t('praxis.charts.editor.specialization.gaugeHint', 'Gauge charts render exactly one bucket and one metric. Remote stats use group-by with limit 1; values outside the scale fail instead of being clamped.') }}\n </p>\n <div class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.gaugeMin', 'Scale minimum') }}</mat-label>\n <input\n matInput\n type=\"number\"\n data-testid=\"chart-editor-gauge-min\"\n [ngModel]=\"gaugeMinValue()\"\n (ngModelChange)=\"setGaugeMin($event)\"\n [disabled]=\"isReadonly()\"\n />\n </mat-form-field>\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.gaugeMax', 'Scale maximum') }}</mat-label>\n <input\n matInput\n type=\"number\"\n data-testid=\"chart-editor-gauge-max\"\n [ngModel]=\"gaugeMaxValue()\"\n (ngModelChange)=\"setGaugeMax($event)\"\n [disabled]=\"isReadonly()\"\n />\n </mat-form-field>\n </div>\n </mat-card-content>\n </mat-card>\n }\n\n @if (showComboPanel()) {\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.specialization.comboTitle', 'Combo guidance') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content>\n <p class=\"editor-caption\">\n {{ t('praxis.charts.editor.specialization.comboHint', 'Combo charts require at least two metrics and allow per-metric axis and series kind mapping.') }}\n </p>\n </mat-card-content>\n </mat-card>\n }\n\n @if (showPieDonutPanel()) {\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.specialization.pieDonutTitle', 'Composition guidance') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content>\n <p class=\"editor-caption\">\n {{ t('praxis.charts.editor.specialization.pieDonutHint', 'Pie, donut, funnel, pyramid and treemap charts keep one metric and use the first dimension as the category segment.') }}\n </p>\n </mat-card-content>\n </mat-card>\n }\n\n @if (showScatterPanel()) {\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.specialization.scatterTitle', 'Scatter guidance') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content>\n <p class=\"editor-caption\">\n {{ t('praxis.charts.editor.specialization.scatterHint', 'With one metric, scatter uses the first dimension as X and the metric as Y. With two metrics, the dimension identifies the grouped bucket and the metrics map to X and Y.') }}\n </p>\n </mat-card-content>\n </mat-card>\n }\n\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.analytics.dimensionsTitle', 'Dimensions') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-stack\">\n @for (dimension of dimensions(); track $index) {\n <div class=\"editor-row-card\" [attr.data-testid]=\"'chart-editor-dimension-row-' + $index\">\n <div class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\" [attr.data-testid]=\"'chart-editor-dimension-field-' + $index\">\n <mat-label>{{ t('praxis.charts.editor.field.dimension', 'Dimension') }}</mat-label>\n @if (fieldOptions('dimension').length) {\n <mat-select [ngModel]=\"dimension.field || ''\" (ngModelChange)=\"setDimensionField($index, $event)\" [disabled]=\"isReadonly()\">\n @for (field of fieldOptions('dimension'); track field.field) {\n <mat-option [value]=\"field.field\">{{ fieldOptionLabel(field) }}</mat-option>\n }\n </mat-select>\n } @else {\n <mat-select [ngModel]=\"dimension.field || ''\" (ngModelChange)=\"setDimensionField($index, $event)\" [disabled]=\"true\"></mat-select>\n <mat-hint>{{ t('praxis.charts.editor.catalog.dimensionFieldsMissing', 'No governed dimensions are available for the current resource and operation.') }}</mat-hint>\n }\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\" [attr.data-testid]=\"'chart-editor-dimension-role-' + $index\">\n <mat-label>{{ t('praxis.charts.editor.field.dimensionRole', 'Dimension role') }}</mat-label>\n <mat-select [ngModel]=\"dimension.role || 'category'\" (ngModelChange)=\"setDimensionRole($index, $event)\" [disabled]=\"isReadonly()\">\n @for (role of dimensionRoles; track role) {\n <mat-option [value]=\"role\">\n {{ t('praxis.charts.editor.dimensionRole.' + role, role) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n </div>\n\n <div class=\"editor-row-actions\">\n <button mat-button type=\"button\" (click)=\"removeDimension($index)\" [disabled]=\"isReadonly() || dimensions().length <= 1\">\n {{ t('praxis.charts.editor.analytics.removeDimension', 'Remove dimension') }}\n </button>\n </div>\n </div>\n }\n\n <div>\n <button mat-stroked-button type=\"button\" data-testid=\"chart-editor-add-dimension\" (click)=\"addDimension()\" [disabled]=\"isReadonly()\">\n {{ t('praxis.charts.editor.analytics.addDimension', 'Add dimension') }}\n </button>\n </div>\n </mat-card-content>\n </mat-card>\n\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.analytics.metricsTitle', 'Metrics') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-stack\">\n @for (metric of metrics(); track $index) {\n <div class=\"editor-row-card\" [attr.data-testid]=\"'chart-editor-metric-row-' + $index\">\n <div class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\" [attr.data-testid]=\"'chart-editor-metric-field-' + $index\">\n <mat-label>{{ t('praxis.charts.editor.field.metric', 'Metric') }}</mat-label>\n @if (fieldOptions('metric').length) {\n <mat-select [ngModel]=\"metric.field || ''\" (ngModelChange)=\"setMetricField($index, $event)\" [disabled]=\"isReadonly()\">\n @for (field of fieldOptions('metric'); track field.field) {\n <mat-option [value]=\"field.field\">{{ fieldOptionLabel(field) }}</mat-option>\n }\n </mat-select>\n } @else {\n <mat-select [ngModel]=\"metric.field || ''\" (ngModelChange)=\"setMetricField($index, $event)\" [disabled]=\"true\"></mat-select>\n <mat-hint>{{ t('praxis.charts.editor.catalog.metricFieldsMissing', 'No governed metrics are available for the current resource and operation.') }}</mat-hint>\n }\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\" [attr.data-testid]=\"'chart-editor-metric-label-' + $index\">\n <mat-label>{{ t('praxis.charts.editor.field.metricLabel', 'Metric label') }}</mat-label>\n <input matInput [ngModel]=\"metric.label || ''\" (ngModelChange)=\"setMetricLabel($index, $event)\" [disabled]=\"isReadonly()\" />\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\" [attr.data-testid]=\"'chart-editor-metric-aggregation-' + $index\">\n <mat-label>{{ t('praxis.charts.editor.field.metricAggregation', 'Aggregation') }}</mat-label>\n <mat-select [ngModel]=\"metric.aggregation || 'sum'\" (ngModelChange)=\"setMetricAggregation($index, $event)\" [disabled]=\"isReadonly()\">\n @for (aggregation of metricAggregationOptions(metric.field); track aggregation) {\n <mat-option [value]=\"aggregation\">\n {{ t('praxis.charts.editor.metricAggregation.' + aggregation, aggregation) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n @if (showMetricAxisControls()) {\n <mat-form-field class=\"editor-field\" appearance=\"outline\" [attr.data-testid]=\"'chart-editor-metric-axis-' + $index\">\n <mat-label>{{ t('praxis.charts.editor.field.metricAxis', 'Axis') }}</mat-label>\n <mat-select [ngModel]=\"metric.axis || 'primary'\" (ngModelChange)=\"setMetricAxis($index, $event)\" [disabled]=\"isReadonly()\">\n @for (axis of metricAxes; track axis) {\n <mat-option [value]=\"axis\">\n {{ t('praxis.charts.editor.metricAxis.' + axis, axis) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n }\n\n @if (showMetricSeriesKindControls()) {\n <mat-form-field class=\"editor-field\" appearance=\"outline\" [attr.data-testid]=\"'chart-editor-metric-series-kind-' + $index\">\n <mat-label>{{ t('praxis.charts.editor.field.metricSeriesKind', 'Series kind') }}</mat-label>\n <mat-select [ngModel]=\"metric.seriesKind || 'bar'\" (ngModelChange)=\"setMetricSeriesKind($index, $event)\" [disabled]=\"isReadonly()\">\n @for (seriesKind of metricSeriesKinds; track seriesKind) {\n <mat-option [value]=\"seriesKind\">\n {{ t('praxis.charts.editor.metricSeriesKind.' + seriesKind, seriesKind) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n }\n </div>\n\n <div class=\"editor-row-actions\">\n <button mat-button type=\"button\" [attr.data-testid]=\"'chart-editor-remove-metric-' + $index\" (click)=\"removeMetric($index)\" [disabled]=\"isReadonly() || metrics().length <= 1\">\n {{ t('praxis.charts.editor.analytics.removeMetric', 'Remove metric') }}\n </button>\n </div>\n </div>\n }\n\n <div>\n <button mat-stroked-button type=\"button\" data-testid=\"chart-editor-add-metric\" (click)=\"addMetric()\" [disabled]=\"isReadonly() || !canAddMetric()\">\n {{ t('praxis.charts.editor.analytics.addMetric', 'Add metric') }}\n </button>\n </div>\n </mat-card-content>\n </mat-card>\n </div>\n }\n\n @case ('events') {\n <div class=\"editor-stack\">\n <mat-card class=\"editor-card\" data-testid=\"chart-editor-event-pointClick\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.events.pointClickTitle', 'Point click') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventAction', 'Action') }}</mat-label>\n <mat-select data-testid=\"chart-editor-event-pointClick-action\" [ngModel]=\"eventAction('pointClick')\" (ngModelChange)=\"setEventAction('pointClick', $event)\" [disabled]=\"isReadonly()\">\n <mat-option value=\"\">{{ t('praxis.charts.editor.events.none', 'None') }}</mat-option>\n @for (action of eventActionOptions; track action) {\n <mat-option [value]=\"action\">\n {{ t('praxis.charts.editor.eventAction.' + action, action) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventTarget', 'Target') }}</mat-label>\n <mat-select data-testid=\"chart-editor-event-pointClick-target\" [ngModel]=\"eventTarget('pointClick')\" (ngModelChange)=\"setEventTarget('pointClick', $event)\" [disabled]=\"isReadonly() || !eventAction('pointClick') || targetCatalogUnavailable(eventAction('pointClick'), 'pointClick')\">\n @for (target of targetOptions(eventAction('pointClick'), 'pointClick'); track target.id) {\n <mat-option [value]=\"target.id\">{{ target.label }}</mat-option>\n }\n </mat-select>\n @if (targetCatalogUnavailable(eventAction('pointClick'), 'pointClick')) {\n <mat-hint>{{ t('praxis.charts.editor.catalog.targetMissing', 'Target catalog is required for this governed event action.') }}</mat-hint>\n }\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventMapping', 'Mapping') }}</mat-label>\n <textarea\n matInput\n data-testid=\"chart-editor-event-pointClick-mapping\"\n rows=\"4\"\n [ngModel]=\"eventMappingText('pointClick')\"\n (ngModelChange)=\"setEventMapping('pointClick', $event)\"\n [disabled]=\"isReadonly() || !eventAction('pointClick')\"\n ></textarea>\n </mat-form-field>\n </mat-card-content>\n </mat-card>\n\n <mat-card class=\"editor-card\" data-testid=\"chart-editor-event-selectionChange\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.events.selectionChangeTitle', 'Selection change') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventAction', 'Action') }}</mat-label>\n <mat-select data-testid=\"chart-editor-event-selectionChange-action\" [ngModel]=\"eventAction('selectionChange')\" (ngModelChange)=\"setEventAction('selectionChange', $event)\" [disabled]=\"isReadonly()\">\n <mat-option value=\"\">{{ t('praxis.charts.editor.events.none', 'None') }}</mat-option>\n @for (action of eventActionOptions; track action) {\n <mat-option [value]=\"action\">\n {{ t('praxis.charts.editor.eventAction.' + action, action) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventTarget', 'Target') }}</mat-label>\n <mat-select data-testid=\"chart-editor-event-selectionChange-target\" [ngModel]=\"eventTarget('selectionChange')\" (ngModelChange)=\"setEventTarget('selectionChange', $event)\" [disabled]=\"isReadonly() || !eventAction('selectionChange') || targetCatalogUnavailable(eventAction('selectionChange'), 'selectionChange')\">\n @for (target of targetOptions(eventAction('selectionChange'), 'selectionChange'); track target.id) {\n <mat-option [value]=\"target.id\">{{ target.label }}</mat-option>\n }\n </mat-select>\n @if (targetCatalogUnavailable(eventAction('selectionChange'), 'selectionChange')) {\n <mat-hint>{{ t('praxis.charts.editor.catalog.targetMissing', 'Target catalog is required for this governed event action.') }}</mat-hint>\n }\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventMapping', 'Mapping') }}</mat-label>\n <textarea\n matInput\n data-testid=\"chart-editor-event-selectionChange-mapping\"\n rows=\"4\"\n [ngModel]=\"eventMappingText('selectionChange')\"\n (ngModelChange)=\"setEventMapping('selectionChange', $event)\"\n [disabled]=\"isReadonly() || !eventAction('selectionChange')\"\n ></textarea>\n </mat-form-field>\n </mat-card-content>\n </mat-card>\n\n <mat-card class=\"editor-card\" data-testid=\"chart-editor-event-drillDown\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.events.drillDownTitle', 'Drill down') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventAction', 'Action') }}</mat-label>\n <mat-select data-testid=\"chart-editor-event-drillDown-action\" [ngModel]=\"eventAction('drillDown')\" (ngModelChange)=\"setEventAction('drillDown', $event)\" [disabled]=\"isReadonly()\">\n <mat-option value=\"\">{{ t('praxis.charts.editor.events.none', 'None') }}</mat-option>\n @for (action of eventActionOptions; track action) {\n <mat-option [value]=\"action\">\n {{ t('praxis.charts.editor.eventAction.' + action, action) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventTarget', 'Target') }}</mat-label>\n <mat-select data-testid=\"chart-editor-event-drillDown-target\" [ngModel]=\"eventTarget('drillDown')\" (ngModelChange)=\"setEventTarget('drillDown', $event)\" [disabled]=\"isReadonly() || !eventAction('drillDown') || targetCatalogUnavailable(eventAction('drillDown'), 'drillDown')\">\n @for (target of targetOptions(eventAction('drillDown'), 'drillDown'); track target.id) {\n <mat-option [value]=\"target.id\">{{ target.label }}</mat-option>\n }\n </mat-select>\n @if (targetCatalogUnavailable(eventAction('drillDown'), 'drillDown')) {\n <mat-hint>{{ t('praxis.charts.editor.catalog.targetMissing', 'Target catalog is required for this governed event action.') }}</mat-hint>\n }\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventMapping', 'Mapping') }}</mat-label>\n <textarea\n matInput\n data-testid=\"chart-editor-event-drillDown-mapping\"\n rows=\"4\"\n [ngModel]=\"eventMappingText('drillDown')\"\n (ngModelChange)=\"setEventMapping('drillDown', $event)\"\n [disabled]=\"isReadonly() || !eventAction('drillDown')\"\n ></textarea>\n </mat-form-field>\n </mat-card-content>\n </mat-card>\n\n <mat-card class=\"editor-card\" data-testid=\"chart-editor-event-crossFilter\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.events.crossFilterTitle', 'Cross filter') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventAction', 'Action') }}</mat-label>\n <mat-select data-testid=\"chart-editor-event-crossFilter-action\" [ngModel]=\"eventAction('crossFilter')\" (ngModelChange)=\"setEventAction('crossFilter', $event)\" [disabled]=\"isReadonly()\">\n <mat-option value=\"\">{{ t('praxis.charts.editor.events.none', 'None') }}</mat-option>\n @for (action of eventActionOptions; track action) {\n <mat-option [value]=\"action\">\n {{ t('praxis.charts.editor.eventAction.' + action, action) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventTarget', 'Target') }}</mat-label>\n <mat-select data-testid=\"chart-editor-event-crossFilter-target\" [ngModel]=\"eventTarget('crossFilter')\" (ngModelChange)=\"setEventTarget('crossFilter', $event)\" [disabled]=\"isReadonly() || !eventAction('crossFilter') || targetCatalogUnavailable(eventAction('crossFilter'), 'crossFilter')\">\n @for (target of targetOptions(eventAction('crossFilter'), 'crossFilter'); track target.id) {\n <mat-option [value]=\"target.id\">{{ target.label }}</mat-option>\n }\n </mat-select>\n @if (targetCatalogUnavailable(eventAction('crossFilter'), 'crossFilter')) {\n <mat-hint>{{ t('praxis.charts.editor.catalog.targetMissing', 'Target catalog is required for this governed event action.') }}</mat-hint>\n }\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventMapping', 'Mapping') }}</mat-label>\n <textarea\n matInput\n data-testid=\"chart-editor-event-crossFilter-mapping\"\n rows=\"4\"\n [ngModel]=\"eventMappingText('crossFilter')\"\n (ngModelChange)=\"setEventMapping('crossFilter', $event)\"\n [disabled]=\"isReadonly() || !eventAction('crossFilter')\"\n ></textarea>\n </mat-form-field>\n </mat-card-content>\n </mat-card>\n </div>\n }\n\n @case ('preview') {\n <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 @if (doc().source.kind === 'derived') {\n <p class=\"editor-caption\" data-testid=\"chart-editor-preview-origin\">\n {{\n chartPreview.dataOrigin === 'local'\n ? t('praxis.charts.editor.preview.originLocal', 'Previewing widget local dataset (inputs.data).')\n : t('praxis.charts.editor.preview.originSynthetic', 'Previewing synthetic sample data (no local dataset records).')\n }}\n </p>\n }\n <praxis-chart [config]=\"chartPreview.config\" [data]=\"chartPreview.data\"></praxis-chart>\n } @else {\n <div class=\"editor-empty\">\n {{ t('praxis.charts.editor.preview.invalid', 'Preview is unavailable while the contract has blocking errors.') }}\n </div>\n }\n </mat-card-content>\n </mat-card>\n </div>\n </div>\n</div>\n", styles: [":host{display:block;min-width:0;container:praxis-chart-editor / inline-size;color:var(--md-sys-color-on-surface, #1a1b20)}.editor-shell{display:grid;gap:18px;min-width:0}.editor-nav{display:flex;gap:8px;flex-wrap:wrap}.editor-nav button.active{background:color-mix(in srgb,var(--md-sys-color-primary, #1263b4) 18%,transparent);color:var(--md-sys-color-primary, #1263b4)}.editor-layout{display:grid;gap:18px;grid-template-columns:minmax(0,1.35fr) minmax(320px,.9fr);align-items:start}.editor-form,.editor-side{display:grid;gap:16px;min-width:0}.editor-card{box-sizing:border-box;width:100%;min-width:0;border-radius:20px;border:1px solid color-mix(in srgb,var(--md-sys-color-outline, #c5c7ce) 72%,transparent);background:linear-gradient(180deg,#1263b408,#1263b400)}.editor-grid{display:grid;gap:14px;grid-template-columns:repeat(auto-fit,minmax(220px,1fr))}.editor-stack{display:grid;gap:14px}.editor-row-card{padding:14px;border-radius:16px;border:1px solid color-mix(in srgb,var(--md-sys-color-outline, #c5c7ce) 54%,transparent);background:color-mix(in srgb,var(--md-sys-color-surface, #fff) 92%,rgba(18,99,180,.04))}.editor-row-actions{display:flex;justify-content:flex-end}.editor-field{width:100%}.editor-issues{display:grid;gap:10px;margin:0;padding:0;list-style:none}.editor-issue{padding:12px 14px;border-radius:14px;background:color-mix(in srgb,var(--md-sys-color-error, #b3261e) 8%,transparent);border:1px solid color-mix(in srgb,var(--md-sys-color-error, #b3261e) 18%,transparent)}.editor-issue strong{display:block;margin-bottom:4px}.editor-caption{margin:0 0 12px;color:var(--md-sys-color-on-surface-variant, #5a5d67);font-size:.92rem}.editor-empty{padding:18px;border-radius:14px;background:color-mix(in srgb,var(--md-sys-color-surface-variant, #eceff4) 78%,transparent)}.editor-side praxis-chart{display:block;width:100%;min-width:0;max-width:100%}@container praxis-chart-editor (max-width: 840px){.editor-layout{grid-template-columns:minmax(0,1fr)}}@media(max-width:960px){.editor-layout{grid-template-columns:minmax(0,1fr)}}\n"] }]
10445
10459
  }], ctorParameters: () => [], propDecorators: { documentInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "document", required: false }] }], localDataInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "localData", required: false }] }], modeInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "mode", required: false }] }], readonlyInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "readonly", required: false }] }], availableResourcesInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "availableResources", required: false }] }], availableFieldsInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "availableFields", required: false }] }], availableTargetsInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "availableTargets", required: false }] }], apply: [{ type: i0.Output, args: ["apply"] }], save: [{ type: i0.Output, args: ["save"] }], resetChange: [{ type: i0.Output, args: ["resetChange"] }], documentChange: [{ type: i0.Output, args: ["documentChange"] }], localDataChange: [{ type: i0.Output, args: ["localDataChange"] }] } });
10446
10460
 
10447
- var praxisChartConfigEditor = /*#__PURE__*/Object.freeze({
10448
- __proto__: null,
10449
- PraxisChartConfigEditor: PraxisChartConfigEditor
10450
- });
10451
-
10452
10461
  class PraxisChartWidgetConfigEditor {
10453
10462
  i18n = inject(PraxisI18nService);
10454
10463
  canonicalMapper = inject(PraxisChartCanonicalContractMapperService);
@@ -11125,6 +11134,11 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
11125
11134
  args: ['chartEditor']
11126
11135
  }] } });
11127
11136
 
11137
+ var praxisChartWidgetConfigEditor = /*#__PURE__*/Object.freeze({
11138
+ __proto__: null,
11139
+ PraxisChartWidgetConfigEditor: PraxisChartWidgetConfigEditor
11140
+ });
11141
+
11128
11142
  const PRAXIS_CHART_PALETTE_DEFAULT_CONFIG = {
11129
11143
  id: 'palette-chart',
11130
11144
  type: 'bar',
package/package.json CHANGED
@@ -1,14 +1,14 @@
1
1
  {
2
2
  "name": "@praxisui/charts",
3
- "version": "10.0.0-rc.20",
3
+ "version": "10.0.0-rc.21",
4
4
  "description": "Metadata-driven charts library for Praxis UI Angular with engine adapters and Apache ECharts as the initial renderer.",
5
5
  "peerDependencies": {
6
6
  "@angular/common": "^21.0.0",
7
7
  "@angular/core": "^21.0.0",
8
- "@praxisui/core": "^10.0.0-rc.20",
8
+ "@praxisui/core": "^10.0.0-rc.21",
9
9
  "@angular/forms": "^21.0.0",
10
10
  "@angular/material": "^21.0.0",
11
- "@praxisui/table": "^10.0.0-rc.20",
11
+ "@praxisui/table": "^10.0.0-rc.21",
12
12
  "rxjs": "~7.8.0"
13
13
  },
14
14
  "dependencies": {
@@ -658,6 +658,7 @@ declare class PraxisChartComponent implements WidgetShellActionContributor {
658
658
  private readonly remoteResolvedData;
659
659
  private readonly remoteRuntimeState;
660
660
  private readonly remoteTechnicalError;
661
+ private readonly runtimeOwner;
661
662
  private readonly runtimeChartDocument;
662
663
  private readonly mappedRuntimeConfig;
663
664
  private readonly chartDocumentMappingError;
@@ -676,13 +677,14 @@ declare class PraxisChartComponent implements WidgetShellActionContributor {
676
677
  private deferredRenderAttempts;
677
678
  private previousRemoteSignature;
678
679
  private previousRemoteDataResolver;
679
- private previousDocumentSignature;
680
680
  private inheritedThemeSignature;
681
681
  private editorSessionSubscriptions;
682
682
  private remoteRequestSubscription;
683
683
  private remoteRequestSequence;
684
684
  private previousSelectionScopeSignature;
685
685
  private readonly userEditedDocument;
686
+ private readonly runtimeData;
687
+ private readonly runtimeQueryContext;
686
688
  readonly effectiveConfig: _angular_core.Signal<PraxisChartConfig>;
687
689
  readonly resolvedData: _angular_core.Signal<PraxisChartDataRow[]>;
688
690
  readonly resolvedHeight: _angular_core.Signal<string>;
@@ -726,6 +728,9 @@ declare class PraxisChartComponent implements WidgetShellActionContributor {
726
728
  private t;
727
729
  canOpenConfigEditor(): boolean;
728
730
  openConfigEditor(): Promise<void>;
731
+ private configEditorSessionSignature;
732
+ private editorInputSignature;
733
+ private configEditorInputs;
729
734
  private resolveConfigEditorContext;
730
735
  private resolveEditorOptions;
731
736
  private captureFocusReturnTarget;