@praxisui/charts 10.0.0-rc.8 → 10.0.0-rc.9

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
@@ -257,6 +257,26 @@ follows the host. Palette inheritance is preserved. Explicit chart surface/backg
257
257
  and text colors remain authoritative; choosing them requires a coherent contrast pair.
258
258
  Changing the host theme updates inherited colors without modifying the chart document.
259
259
 
260
+ Existing color strings in palette arrays, metric/series colors, surface backgrounds,
261
+ text colors and gauge bands can reference host CSS custom properties (for example,
262
+ `var(--md-sys-color-primary, #1263b4)`). The default engine resolves them against the
263
+ chart host on every render and projects modern CSS colors such as OKLCH into sRGB
264
+ for Canvas. Explicit literal colors and palette overrides retain precedence. Missing
265
+ or invalid tokens use bounded engine defaults; gauge bands retain their semantic
266
+ tone fallback. Persisted documents and source data remain unchanged. A string
267
+ `theme.palette` in `x-ui.chart` still names a registered Praxis palette; use an array
268
+ for individual CSS colors.
269
+
270
+ Palette array items must be non-empty strings; malformed persisted values are
271
+ rejected by canonical validation with the item path. Direct runtime color fields
272
+ also fall back safely when untyped JSON supplies another value type. Color probes
273
+ isolate their measured color from host CSS rules, including rules with `!important`.
274
+
275
+ The engine also derives tooltip background, foreground and border from the host's
276
+ existing popover roles, with card/foreground and Material fallbacks. Tooltip
277
+ formatting, triggers and disabled state keep their authored semantics; these
278
+ computed appearance values are transient and never saved into the chart document.
279
+
260
280
  ## Micro Visualizations
261
281
 
262
282
  Transparent charts inside `WidgetShell` inherit the shell foreground, including fixed dark finishes in a light application theme. The chart's opaque accessible-data panel pairs its own background with the corresponding foreground. Explicit chart text colors keep precedence.
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "schemaVersion": "1.0.0",
3
- "generatedAt": "2026-10-12T02:00:03.764Z",
3
+ "generatedAt": "2026-10-12T12:49:05.370Z",
4
4
  "packageName": "@praxisui/charts",
5
- "packageVersion": "10.0.0-rc.8",
5
+ "packageVersion": "10.0.0-rc.9",
6
6
  "sourceRegistry": "praxis-component-registry-ingestion",
7
7
  "sourceRegistryVersion": "1.0.0",
8
8
  "componentCount": 4,
@@ -10,7 +10,7 @@ import * as i1$1 from '@angular/common/http';
10
10
  import { HttpErrorResponse } from '@angular/common/http';
11
11
  import { catchError } from 'rxjs/operators';
12
12
  import { AnalyticsTableConfigAdapterService, AnalyticsTableStatsApiService, ANALYTICS_TABLE_ROW_KEY_FIELD, PraxisTable } from '@praxisui/table';
13
- import { use, init } from 'echarts/core';
13
+ import { color, use, init } from 'echarts/core';
14
14
  import { BarChart, FunnelChart, GaugeChart, LineChart, PieChart, ScatterChart, TreemapChart } from 'echarts/charts';
15
15
  import { AriaComponent, DatasetComponent, GridComponent, LegendComponent, TimelineComponent, TitleComponent, TooltipComponent, TransformComponent } from 'echarts/components';
16
16
  import { CanvasRenderer } from 'echarts/renderers';
@@ -718,11 +718,24 @@ class ChartContractValidationService {
718
718
  }
719
719
  }
720
720
  validateTheme(document, issues) {
721
- if (document.theme?.palette && typeof document.theme.palette === 'string') {
722
- if (!isPraxisChartPaletteToken(document.theme.palette)) {
723
- issues.push(this.error('palette-token-unknown', 'theme.palette', `x-ui.chart theme.palette token "${document.theme.palette}" is not registered in @praxisui/charts.`));
721
+ const palette = document.theme?.palette;
722
+ if (palette === undefined)
723
+ return;
724
+ if (Array.isArray(palette)) {
725
+ palette.forEach((color, index) => {
726
+ if (typeof color !== 'string' || !color.trim()) {
727
+ issues.push(this.error('palette-color-invalid', `theme.palette[${index}]`, `x-ui.chart theme.palette[${index}] must be a non-empty CSS color string.`));
728
+ }
729
+ });
730
+ }
731
+ else if (typeof palette === 'string') {
732
+ if (!isPraxisChartPaletteToken(palette)) {
733
+ issues.push(this.error('palette-token-unknown', 'theme.palette', `x-ui.chart theme.palette token "${palette}" is not registered in @praxisui/charts.`));
724
734
  }
725
735
  }
736
+ else {
737
+ issues.push(this.error('palette-invalid', 'theme.palette', 'x-ui.chart theme.palette must be a registered palette token or an array of CSS color strings.'));
738
+ }
726
739
  }
727
740
  validateMetrics(document, issues) {
728
741
  if (!document.metrics?.length) {
@@ -2431,6 +2444,7 @@ class PraxisChartComponent {
2431
2444
  shellObserver = signal(null, ...(ngDevMode ? [{ debugName: "shellObserver" }] : /* istanbul ignore next */ []));
2432
2445
  themeObserver = signal(null, ...(ngDevMode ? [{ debugName: "themeObserver" }] : /* istanbul ignore next */ []));
2433
2446
  inheritedTheme = signal(null, ...(ngDevMode ? [{ debugName: "inheritedTheme" }] : /* istanbul ignore next */ []));
2447
+ hostThemeRevision = signal(0, ...(ngDevMode ? [{ debugName: "hostThemeRevision" }] : /* istanbul ignore next */ []));
2434
2448
  inheritedHostTextColor = signal(undefined, ...(ngDevMode ? [{ debugName: "inheritedHostTextColor" }] : /* istanbul ignore next */ []));
2435
2449
  currentLoadState = signal('idle', ...(ngDevMode ? [{ debugName: "currentLoadState" }] : /* istanbul ignore next */ []));
2436
2450
  remoteResolvedData = signal(null, ...(ngDevMode ? [{ debugName: "remoteResolvedData" }] : /* istanbul ignore next */ []));
@@ -2877,6 +2891,7 @@ class PraxisChartComponent {
2877
2891
  });
2878
2892
  effect(() => {
2879
2893
  this.renderAttempt();
2894
+ this.hostThemeRevision();
2880
2895
  if (this.loadState() !== 'ready') {
2881
2896
  this.cancelScheduledRender();
2882
2897
  this.cancelScheduledResize();
@@ -3477,6 +3492,9 @@ class PraxisChartComponent {
3477
3492
  this.themeObserver()?.disconnect();
3478
3493
  const observer = new MutationObserver(() => {
3479
3494
  this.syncInheritedTheme(host);
3495
+ // Token-only changes can leave the inherited theme document unchanged.
3496
+ // The adapter must still resolve authored CSS colors against the new host.
3497
+ this.hostThemeRevision.update((revision) => revision + 1);
3480
3498
  });
3481
3499
  let current = host;
3482
3500
  while (current) {
@@ -5744,6 +5762,173 @@ function stringifyValue(value) {
5744
5762
  return value === null || value === undefined ? undefined : String(value);
5745
5763
  }
5746
5764
 
5765
+ const FALLBACK_PALETTE = ['#1263b4', '#0f766e', '#f08c00', '#c92a2a', '#7b61ff'];
5766
+ const GAUGE_TONE_COLORS = {
5767
+ success: ['var(--praxis-success, var(--ready, #1b7a43))', '#1b7a43'],
5768
+ warning: ['var(--praxis-warning, var(--warning, #d97706))', '#d97706'],
5769
+ danger: ['var(--destructive, var(--md-sys-color-error, #c62828))', '#c62828'],
5770
+ critical: ['var(--destructive, var(--md-sys-color-error, #b91c1c))', '#b91c1c'],
5771
+ info: ['var(--praxis-info, var(--category-sky, var(--md-sys-color-primary, #0a6eb4)))', '#0a6eb4'],
5772
+ neutral: ['var(--muted-foreground, var(--md-sys-color-on-surface-variant, #64748b))', '#64748b'],
5773
+ };
5774
+ let probeSequence = 0;
5775
+ /** Resolve only canonical color fields; never traverse source rows or raw engine options. */
5776
+ function materializeHostColors(host, config) {
5777
+ const document = host.ownerDocument;
5778
+ const view = document?.defaultView;
5779
+ const probe = view ? document.createElement('span') : undefined;
5780
+ const property = `--praxis-chart-engine-color-${++probeSequence}`;
5781
+ const cache = new Map();
5782
+ let context;
5783
+ if (probe) {
5784
+ probe.style.cssText = 'display:none;pointer-events:none';
5785
+ host.appendChild(probe);
5786
+ }
5787
+ const resolve = (value, fallback) => {
5788
+ if (value === undefined)
5789
+ return undefined;
5790
+ // Runtime config may originate from persisted JSON rather than typed TS.
5791
+ if (typeof value !== 'string')
5792
+ return fallback;
5793
+ if (cache.has(value))
5794
+ return cache.get(value) ?? fallback;
5795
+ let color;
5796
+ const candidate = value.trim();
5797
+ if (probe && view) {
5798
+ // The browser expands nested var() fallbacks and rejects cycles. A private,
5799
+ // per-render property prevents invalid substitutions from inheriting color.
5800
+ probe.style.removeProperty(property);
5801
+ probe.style.setProperty(property, candidate, 'important');
5802
+ const expanded = probe.style.getPropertyValue(property).trim()
5803
+ ? view.getComputedStyle(probe).getPropertyValue(property).trim()
5804
+ : '';
5805
+ probe.style.removeProperty('color');
5806
+ // Host rules targeting spans must not replace the value being measured.
5807
+ probe.style.setProperty('color', expanded, 'important');
5808
+ if (probe.style.color) {
5809
+ color = isEngineColor(candidate) ? value : view.getComputedStyle(probe).color;
5810
+ }
5811
+ }
5812
+ else if (isEngineColor(candidate)) {
5813
+ color = value;
5814
+ }
5815
+ if (color && !isEngineColor(color)) {
5816
+ try {
5817
+ if (context === undefined) {
5818
+ const canvas = document.createElement('canvas');
5819
+ canvas.width = canvas.height = 1;
5820
+ context = canvas.getContext('2d', { willReadFrequently: true });
5821
+ }
5822
+ if (context) {
5823
+ // ECharts' color parser does not understand OKLCH, color() or modern
5824
+ // color-mix(). Canvas supplies their browser-defined sRGB projection.
5825
+ context.fillStyle = '#010203';
5826
+ context.fillStyle = color;
5827
+ const first = context.fillStyle;
5828
+ context.fillStyle = '#040506';
5829
+ context.fillStyle = color;
5830
+ if (context.fillStyle !== first) {
5831
+ color = undefined;
5832
+ }
5833
+ else {
5834
+ context.clearRect(0, 0, 1, 1);
5835
+ context.fillRect(0, 0, 1, 1);
5836
+ const [r, g, b, alpha] = context.getImageData(0, 0, 1, 1).data;
5837
+ color = `rgba(${r}, ${g}, ${b}, ${alpha / 255})`;
5838
+ }
5839
+ }
5840
+ else {
5841
+ color = undefined;
5842
+ }
5843
+ }
5844
+ catch {
5845
+ color = undefined;
5846
+ }
5847
+ }
5848
+ cache.set(value, color);
5849
+ return color ?? fallback;
5850
+ };
5851
+ try {
5852
+ const rawPalette = config.theme?.palette;
5853
+ const palette = Array.isArray(rawPalette)
5854
+ ? rawPalette.map((color, index) => {
5855
+ const fallback = FALLBACK_PALETTE[index % FALLBACK_PALETTE.length];
5856
+ return resolve(color, fallback) ?? fallback;
5857
+ })
5858
+ : undefined;
5859
+ const seriesPalette = palette?.length ? palette : FALLBACK_PALETTE;
5860
+ const hostText = view ? resolve(view.getComputedStyle(host).color, '#4b5563') : '#4b5563';
5861
+ const resolvedConfig = {
5862
+ ...config,
5863
+ ...(config.theme ? { theme: {
5864
+ ...config.theme,
5865
+ palette,
5866
+ backgroundColor: resolve(config.theme.backgroundColor, 'transparent'),
5867
+ textColor: resolve(config.theme.textColor, hostText),
5868
+ ...(config.theme.surface ? { surface: {
5869
+ ...config.theme.surface,
5870
+ background: resolve(config.theme.surface.background, 'transparent'),
5871
+ borderColor: resolve(config.theme.surface.borderColor, hostText),
5872
+ } } : {}),
5873
+ } } : {}),
5874
+ series: config.series.map((series, index) => ({
5875
+ ...series,
5876
+ color: resolve(series.color, seriesPalette[index % seriesPalette.length]),
5877
+ })),
5878
+ ...(config.gauge ? { gauge: {
5879
+ ...config.gauge,
5880
+ colorBands: config.gauge.colorBands?.map((band) => {
5881
+ const tone = band.tone ? GAUGE_TONE_COLORS[band.tone] : undefined;
5882
+ const toneColor = tone ? resolve(tone[0], tone[1]) : undefined;
5883
+ return { ...band, color: band.color === undefined ? toneColor : resolve(band.color, toneColor) };
5884
+ }),
5885
+ } } : {}),
5886
+ };
5887
+ return {
5888
+ config: resolvedConfig,
5889
+ tooltip: {
5890
+ backgroundColor: resolve('var(--popover, var(--card, var(--md-sys-color-surface-container, #fff)))', '#fff'),
5891
+ textColor: resolve('var(--popover-foreground, var(--foreground, var(--md-sys-color-on-surface, #1f2937)))', '#1f2937'),
5892
+ borderColor: resolve('var(--border, var(--md-sys-color-outline-variant, #d1d5db))', '#d1d5db'),
5893
+ },
5894
+ };
5895
+ }
5896
+ finally {
5897
+ probe?.remove();
5898
+ }
5899
+ }
5900
+ /** Apply host chrome only to tooltips the compiler enabled, preserving all callbacks. */
5901
+ function materializeTooltipColors(option, colors) {
5902
+ const apply = (tooltip) => {
5903
+ const entries = Array.isArray(tooltip) ? tooltip : [tooltip];
5904
+ const resolved = entries.map((entry) => ({
5905
+ ...entry,
5906
+ backgroundColor: colors.backgroundColor,
5907
+ borderColor: colors.borderColor,
5908
+ textStyle: { ...entry.textStyle, color: colors.textColor },
5909
+ }));
5910
+ return Array.isArray(tooltip) ? resolved : resolved[0];
5911
+ };
5912
+ const applyUnit = (unit) => ({
5913
+ ...unit,
5914
+ ...(unit.tooltip ? { tooltip: apply(unit.tooltip) } : {}),
5915
+ });
5916
+ return {
5917
+ ...option,
5918
+ ...(option.tooltip ? { tooltip: apply(option.tooltip) } : {}),
5919
+ ...(option.baseOption ? { baseOption: applyUnit(option.baseOption) } : {}),
5920
+ ...(option.media ? { media: option.media.map((entry) => ({
5921
+ ...entry,
5922
+ ...(entry.option ? { option: applyUnit(entry.option) } : {}),
5923
+ })) } : {}),
5924
+ };
5925
+ }
5926
+ /** Ask the renderer's own parser rather than maintaining a parallel CSS dialect. */
5927
+ function isEngineColor(value) {
5928
+ const channels = color.parse(value);
5929
+ return Boolean(channels?.length === 4 && channels.every(Number.isFinite));
5930
+ }
5931
+
5747
5932
  const PRAXIS_CHART_DEFAULT_PALETTE = ['#1263b4', '#0f766e', '#f08c00', '#c92a2a', '#7b61ff'];
5748
5933
  const TITLE_TOP = 18;
5749
5934
  const TITLE_LEFT = 24;
@@ -6730,7 +6915,8 @@ class EChartsEngineAdapter {
6730
6915
  this.chart = this.createChart(host);
6731
6916
  this.currentHost = host;
6732
6917
  }
6733
- const option = this.optionBuilder.build(payload.config, payload.data);
6918
+ const appearance = materializeHostColors(host, payload.config);
6919
+ const option = materializeTooltipColors(this.optionBuilder.build(appearance.config, payload.data), appearance.tooltip);
6734
6920
  const chart = this.chart;
6735
6921
  chart.setOption(option, true);
6736
6922
  this.detachHandlers();
@@ -9130,8 +9316,7 @@ class PraxisChartConfigEditor {
9130
9316
  }));
9131
9317
  }
9132
9318
  setPalette(value) {
9133
- const palette = value
9134
- .split(/[\n,]/)
9319
+ const palette = this.splitPaletteColors(value)
9135
9320
  .map((item) => item.trim())
9136
9321
  .filter(Boolean);
9137
9322
  this.patchDocument((document) => ({
@@ -9784,6 +9969,24 @@ class PraxisChartConfigEditor {
9784
9969
  this.currentDocument.set(updater(structuredClone(this.currentDocument())));
9785
9970
  this.refreshState(true);
9786
9971
  }
9972
+ splitPaletteColors(value) {
9973
+ const colors = [];
9974
+ let depth = 0;
9975
+ let start = 0;
9976
+ for (let index = 0; index < value.length; index += 1) {
9977
+ const character = value[index];
9978
+ if (character === '(')
9979
+ depth += 1;
9980
+ if (character === ')')
9981
+ depth = Math.max(0, depth - 1);
9982
+ if (depth === 0 && (character === ',' || character === '\n')) {
9983
+ colors.push(value.slice(start, index));
9984
+ start = index + 1;
9985
+ }
9986
+ }
9987
+ colors.push(value.slice(start));
9988
+ return colors;
9989
+ }
9787
9990
  refreshState(markDirty) {
9788
9991
  this.isDirty$.next(markDirty);
9789
9992
  this.isValid$.next(this.validation().valid);
package/package.json CHANGED
@@ -1,14 +1,14 @@
1
1
  {
2
2
  "name": "@praxisui/charts",
3
- "version": "10.0.0-rc.8",
3
+ "version": "10.0.0-rc.9",
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.8",
8
+ "@praxisui/core": "^10.0.0-rc.9",
9
9
  "@angular/forms": "^21.0.0",
10
10
  "@angular/material": "^21.0.0",
11
- "@praxisui/table": "^10.0.0-rc.8",
11
+ "@praxisui/table": "^10.0.0-rc.9",
12
12
  "rxjs": "~7.8.0"
13
13
  },
14
14
  "dependencies": {
@@ -652,6 +652,7 @@ declare class PraxisChartComponent implements WidgetShellActionContributor {
652
652
  private readonly shellObserver;
653
653
  private readonly themeObserver;
654
654
  private readonly inheritedTheme;
655
+ private readonly hostThemeRevision;
655
656
  private readonly inheritedHostTextColor;
656
657
  private readonly currentLoadState;
657
658
  private readonly remoteResolvedData;
@@ -1647,6 +1648,7 @@ declare class PraxisChartConfigEditor implements SettingsValueProvider {
1647
1648
  private createApplyPayload;
1648
1649
  private createSavePayload;
1649
1650
  private patchDocument;
1651
+ private splitPaletteColors;
1650
1652
  private refreshState;
1651
1653
  static ɵfac: _angular_core.ɵɵFactoryDeclaration<PraxisChartConfigEditor, never>;
1652
1654
  static ɵcmp: _angular_core.ɵɵComponentDeclaration<PraxisChartConfigEditor, "praxis-chart-config-editor", never, { "documentInput": { "alias": "document"; "required": false; "isSignal": true; }; "modeInput": { "alias": "mode"; "required": false; "isSignal": true; }; "readonlyInput": { "alias": "readonly"; "required": false; "isSignal": true; }; "availableResourcesInput": { "alias": "availableResources"; "required": false; "isSignal": true; }; "availableFieldsInput": { "alias": "availableFields"; "required": false; "isSignal": true; }; "availableTargetsInput": { "alias": "availableTargets"; "required": false; "isSignal": true; }; }, { "apply": "apply"; "save": "save"; "resetChange": "resetChange"; "documentChange": "documentChange"; }, never, never, true, never>;