Skip to content

feat(ui-core): ECharts-based /charts module to replace recharts - #34273

Merged
karanh37 merged 12 commits into
mainfrom
feat/core-charts-echarts
Sep 30, 2026
Merged

karanh37 merged 12 commits into
mainfrom
feat/core-charts-echarts

Conversation

@karanh37

@karanh37 karanh37 commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor

Describe your changes:

Fixes

I added an ECharts-based chart module to @openmetadata/ui-core-components, exposed as a new @openmetadata/ui-core-components/charts entry. This is phase 0 of removing recharts from both OpenMetadata and Collate. The goal is one shared chart layer, with one palette, one theme and one set of components, instead of about 50 hand-built recharts charts spread across openmetadata-ui and collate-ui. Nothing uses the module yet. Later PRs migrate the charts area by area, then drop the recharts dependency.

  • Palette: 15 categorical series colours, the same list and order Collate's AI charts already record.
  • Theme: chrome colours (axes, grid, tooltip, borders) for light and dark mode.
  • Option builders (charts/options/): plain functions that turn data-shaped props into an ECharts option, for line, area, bar, composed and pie charts. They import no React and touch no DOM, so Collate's server-side PNG renderer can run them in Node. A test guards this.
  • EChart: a wrapper around echarts-for-react/lib/core with a tree-shaken echarts/core and the SVG renderer.
    • Detects dark mode from a .dark-mode class on any ancestor or on <html>.
    • Shows a core Skeleton while loading, and an empty state that reuses label.no-data-found, so there are no new i18n keys.
    • Gives screen readers a description built from the caller's translated ariaLabel and the series names.
  • Typed components: LineChart, AreaChart, BarChart, ComposedChart and PieChart. Their props are shaped like the data, so migrating a recharts chart is mostly mapping props. Each also accepts an option override for unusual cases.
  • GeoMapChart: a generic choropleth. The caller supplies everything map-specific: the GeoJSON (already loaded and trimmed), the mapName it is registered under, and an optional resolveRegion that turns raw values such as CA or 06 into feature names. Core bundles no map data or region tables. Collate's chart renderer keeps its USA and world maps, region aliases and inset trimming, and passes them in.
    • Rows naming the same region are summed.
    • Raw values that match no region are reported through onUnmatchedRegions; when nothing matches, the chart shows its empty state.
    • The colour scale spans the data's own lowest and highest values. Regions without data take the theme's empty fill.
    • onRegionClick reports clicks. buildGeoMapOption stays Node-safe.

Type of change:

  • New feature

High-level design:

  • Two layers. EChart renders any ECharts option with the shared theme and the shared loading, empty and accessibility handling. The typed components are built on top of it and cover the chart shapes the apps use today:
    • series cycling through the palette, stacking, a second y-axis, time axes
    • horizontal bars, per-bar colours, value labels, donut with centre label
    • reference lines (for recharts ReferenceLine) and an optional zoom slider (for recharts Brush)
    • choropleth maps over caller-supplied geometry
  • Own sub-path entry. /charts has its own exports entry and is not re-exported from the root. Apps that render no chart never download echarts. echarts and echarts-for-react are optional peer dependencies and are external in the Vite build; dist/charts/index.es.js is about 15 KB.
  • ECharts types. Everything passed through to ECharts uses echarts' own TypeScript types. ChartOption is a ComposeOption<…> limited to the chart types and components we register, so an option using anything else fails the type check.
  • Re-render behaviour. Updates use notMerge: false with replaceMerge: ['series', 'xAxis', 'yAxis', 'grid', 'dataZoom']:
    • Series the user hid through the legend stay hidden.
    • A setting the caller removes (area fill, tooltip formatter, reference lines) goes away instead of lingering.
    • The zoom window the user picked is re-applied when the option is rebuilt.
    • Reference lines sit on their own hidden series, so hiding a data series does not remove them.
    • A colour-scale (visualMap) setting the caller removes, or a switch from a map to another chart type on the same EChart, is cleared.
  • Alternatives considered.
    • Thin wrapper only, with each chart writing raw ECharts options: rejected, because it repeats config across about 50 charts.
    • Typed components only: rejected, because the unusual charts outgrow them.
    • CSS variables straight into the SVG: rejected, because ECharts derives colours for gradients and hover states and would break.
  • Rollout. Nothing changes behaviour until charts are migrated in later PRs. Those migrations will add echarts / echarts-for-react to openmetadata-ui; collate-ui already has them.

Tests:

Use cases covered

  • Line, area, bar (vertical and horizontal), composed and pie charts build the expected ECharts options from rows and series definitions.
  • Missing or non-numeric values show as gaps, not zeros. A pie whose slices are all zero shows the empty state.
  • Dark mode is picked up from <html> or from a scoped .dark-mode ancestor, and re-themes when <html> changes.
  • A clicked point or slice maps back to the caller's row and series key, including horizontal bars and time axes.
  • Against a real server-side ECharts instance: legend selection survives a re-render; removed reference lines, area fills and tooltip formatters are cleared; the zoom window is kept; reference lines stay when the first series is hidden.
  • Geo map: region values resolve through the caller's resolver and are summed; unmatched values are reported once per change, even when the parent passes inline callbacks; the map is registered once per name; on a real ECharts instance the lowest and highest regions take the two ends of the colour range, and a removed colour-scale setting is cleared.

Unit tests

  • I added unit tests for the new/changed logic.
  • Files added: src/charts/**/*.test.ts(x): palette, theme, format, options/merge, options/cartesian, options/pie, options/geo, echart, components, geo-map-chart, node-safety, echarts-integration (136 tests). Full core suite: 553 passing.

Backend integration tests

  • Not applicable (no backend API changes).

Ingestion integration tests

  • Not applicable (no ingestion changes).

Playwright (UI) tests

  • Not applicable. The module has no consumers yet, so no app screen changes.

Manual testing performed

  1. yarn test, yarn type-check, yarn lint, yarn build and yarn build-storybook in openmetadata-ui-core-components/src/main/resources/ui.
  2. Checked the built dist/charts/index.es.js: echarts is imported, not bundled, and the root entry has no echarts.
  3. Reviewed the new Charts/Overview Storybook stories in light, dark and side-by-side modes: line, time axis with zoom, stacked area, bar with reference line, horizontal diverging bar, composed with two y-axes, pie, donut with centre label, geo map (a made-up region grid with region codes resolved by the caller), empty, loading and raw EChart.
  4. Compiled the option builders and their types against Collate's chart renderer dependencies, which have no React installed.

UI screen recording / screenshots:

New Charts/Overview stories in Storybook. The module has no consumers yet, so no app screen changes.

Checklist:

  • I have read the CONTRIBUTING document.

  • My PR title is Fixes <issue-number>: <short explanation>

  • My PR is linked to a GitHub issue via Fixes #<issue-number> above.

  • I have commented on my code, particularly in hard-to-understand areas.

  • For JSON Schema changes: I updated the migration scripts or explained why it is not needed.

  • For UI changes: I attached a screen recording and/or screenshots above.

  • I have added tests (unit / integration / Playwright as applicable) and listed them above.

  • The issue properly describes why the new feature is needed, what's the goal, and how we are building it. Any discussion or decision-making process is reflected in the issue.

  • I have updated the documentation.

  • I have added tests around the new logic.

🤖 Generated with Claude Code

…oss re-renders

- Reference lines ride on their own hidden series on the first axes, so
  hiding a data series or reordering a composed chart no longer moves or
  removes them.
- Series always emit areaStyle and tooltip, so a re-render that drops them
  clears the old ones under replaceMerge.
- dataZoom has fixed ids and EChart re-applies the user's window on rebuild.
- Horizontal bars list the first row at the top (inverse category axis).
- toNumberOrNull only accepts numbers and non-blank numeric strings.
- Component props (ReactNode) move to props.ts so the option builders and
  their types compile without React for the headless renderer.
Copilot AI balanced review requested due to automatic review settings September 30, 2026 07:13
@karanh37 karanh37 added UI UI specific issues safe to test Add this label to run secure Github workflows on PRs labels Sep 30, 2026
@github-actions

Copy link
Copy Markdown
Contributor

❌ PR checklist incomplete

This PR cannot be merged until the following are addressed on its linked issue:

  • No GitHub issue is linked. Link an issue in the Development section of the PR (or add Fixes #12345 to the description). For a same-org cross-repo issue, add Fixes open-metadata/<repo>#123 to the description.

The fields live on the linked issue in the Shipping project (open the issue → right sidebar → Projects). After you set them, re-run this check (or push a commit) — issue/project changes do not re-trigger it automatically.

Maintainers can bypass this check by adding the skip-pr-checks label.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Comment on lines +92 to +93
valueFormatter: (value) => formatTooltipValue(value),
...(props.formatter ? { formatter: props.formatter } : {}),

@gitar-bot gitar-bot Bot Sep 30, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Bug: Removed tooltip formatter/option overrides linger after re-render

EChart updates with notMerge: false, and only series, xAxis, yAxis, grid and dataZoom are in replaceMerge. tooltipConfig adds formatter through a conditional spread (...(props.formatter ? { formatter } : {})), so the key is simply missing when a caller drops tooltip.formatter. ECharts' component merge leaves missing keys alone, so the old top-level formatter keeps rendering. The PR says removed tooltip formatters are cleared, but the integration test only covers the per-series valueFormatter. The same happens to any tooltip/legend/aria keys a caller later removes from the option override. Fix: always write the key (formatter: props.formatter). ECharts merges with overwrite, so an explicit undefined clears the old value, which is the same approach areaStyle/series tooltip already use. Optionally, add tooltip to REPLACE_MERGE_KEYS.

Always set formatter so a removal clears it on merge:

  valueFormatter: (value) => formatTooltipValue(value),
  // Always set, so a re-render that drops the formatter clears the old one.
  formatter: props.formatter,
});

Was this helpful? React with 👍 / 👎

Comment on lines +51 to +64
export const hexToRgba = (hex: string, alpha: number): string => {
const clean = hex.replace('#', '');
const full =
clean.length === 3
? clean
.split('')
.map((c) => c + c)
.join('')
: clean;
const channel = (start: number) =>
Number.parseInt(full.slice(start, start + 2), 16) || 0;

return `rgba(${channel(0)}, ${channel(2)}, ${channel(4)}, ${alpha})`;
};

@gitar-bot gitar-bot Bot Sep 30, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Edge Case: hexToRgba produces wrong area fill for non-hex series colours

ChartSeries.color accepts any string, but areaGradient sends it through hexToRgba, which only understands #RGB/#RRGGBB. Any other value, such as rgb(...), hsl(...) or a named colour, gets parsed as hex. For example, 'red' becomes 'rreedd' and gives rgba(0, 238, 221, 0.2), while rgb(...) falls back to black. So an area chart with such a colour draws a fill that doesn't match its line. Fix: return non-hex input unchanged and use opacity on areaStyle, or validate the input with a /^#([0-9a-f]{3}|[0-9a-f]{6})$/i check and fall back to the raw colour plus areaStyle.opacity.

Only convert real hex values; leave other colours untouched:

export const hexToRgba = (hex: string, alpha: number): string => {
  if (!/^#([0-9a-f]{3}|[0-9a-f]{6})$/i.test(hex)) {
    return hex;
  }
  const clean = hex.slice(1);
  const full = clean.length === 3 ? clean.replace(/./g, (c) => c + c) : clean;
  const channel = (start: number) =>
    Number.parseInt(full.slice(start, start + 2), 16);

  return `rgba(${channel(0)}, ${channel(2)}, ${channel(4)}, ${alpha})`;
};

Was this helpful? React with 👍 / 👎

@github-actions

Copy link
Copy Markdown
Contributor

Jest test Coverage

UI tests summary

Lines Statements Branches Functions
Coverage: 74%
74.16% (109297/147378) 59.45% (66987/112663) 60.86% (22198/36470)

@github-actions

github-actions Bot commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor

✅ Playwright Results — workflow succeeded

Validated commit 905d1ceb981347ac5a86e8860b9ff02e821bf85b in Playwright run 36694051262, attempt 1.

✅ 40 passed · ❌ 0 failed · 🟡 0 flaky · ⏭️ 0 skipped · 🧰 0 lifecycle flaky

Performance

Blocking targets: ✅ met · Optimization targets: 🟡 in progress

Shard-job maxima below are not the full workflow wall time; the linked run includes build, fixture, planning, and reporting.

🕒 Full workflow signal wall (to summary) 21m 41s

⏱️ Max setup 2m 46s · max shard execution 3m 0s · max shard-job elapsed before upload 6m 5s · reporting 2s

🌐 162.00 requests/attempt · 1.25 app boots/UI scenario · 0.00% common-shard skew

Optimization targets still in progress:

  • Application boot ratio was 1.25 per UI scenario (50 boots / 40 scenarios; convergence target: at most 1).
Shard Passed Failed Flaky Skipped Lifecycle failed Lifecycle flaky
✅ Shard chromium-01 40 0 0 0 0 0

📦 Download artifacts

How to debug locally
# Download playwright-test-results-<shard> artifact and unzip
npx playwright show-trace path/to/trace.zip    # view trace

GeoMapChart draws a choropleth over caller-supplied geometry. The caller owns
the map data: loading and trimming the GeoJSON, and resolving raw region values
to feature names through resolveRegion. Rows are summed per region, raw values
that match no feature are reported through onUnmatchedRegions, and the colour
scale spans the data's own range. buildGeoMapOption stays Node-safe for the
headless renderer.
- onUnmatchedRegions reports on mount and when the unmatched set changes,
  never on every parent render, so inline callbacks cannot loop.
- visualMap joins the replaceMerge keys, so a removed colour-scale setting,
  or a switch from a geo option to bars on the same chart, clears it.
- Map regions no longer stay selected after a click (selectedMode false).
Copilot AI balanced review requested due to automatic review settings September 30, 2026 08:32

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

The Storybook toolbar theme toggle already covers light and dark for every story.
Copilot AI balanced review requested due to automatic review settings September 30, 2026 08:59

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Adds a choropleth over simplified public-domain US state geometry, with Alaska and Hawaii as insets and postal codes resolved to state names.
Copilot AI balanced review requested due to automatic review settings September 30, 2026 09:07

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@gitar-bot

gitar-bot Bot commented Sep 30, 2026

Copy link
Copy Markdown
Code Review 👍 Approved with suggestions 0 closed / 2 findings

🟡 Medium risk · Adds a shared charting module and new public chart APIs for later migrations.

Introduces a comprehensive ECharts-based chart module with typed components, theme support, and server-side rendering compatibility. Two minor issues to address: tooltip formatter and other top-level option overrides linger after re-render because missing keys are not explicitly set to undefined, and hexToRgba produces incorrect area fills for non-hex colour values like rgb(...) or named colours.

💡 Bug: Removed tooltip formatter/option overrides linger after re-render

📄 openmetadata-ui-core-components/src/main/resources/ui/src/charts/options/common.ts:92-93 📄 openmetadata-ui-core-components/src/main/resources/ui/src/charts/options/merge.ts:42-48

EChart updates with notMerge: false, and only series, xAxis, yAxis, grid and dataZoom are in replaceMerge. tooltipConfig adds formatter through a conditional spread (...(props.formatter ? { formatter } : {})), so the key is simply missing when a caller drops tooltip.formatter. ECharts' component merge leaves missing keys alone, so the old top-level formatter keeps rendering. The PR says removed tooltip formatters are cleared, but the integration test only covers the per-series valueFormatter. The same happens to any tooltip/legend/aria keys a caller later removes from the option override. Fix: always write the key (formatter: props.formatter). ECharts merges with overwrite, so an explicit undefined clears the old value, which is the same approach areaStyle/series tooltip already use. Optionally, add tooltip to REPLACE_MERGE_KEYS.

Always set formatter so a removal clears it on merge
  valueFormatter: (value) => formatTooltipValue(value),
  // Always set, so a re-render that drops the formatter clears the old one.
  formatter: props.formatter,
});
💡 Edge Case: hexToRgba produces wrong area fill for non-hex series colours

📄 openmetadata-ui-core-components/src/main/resources/ui/src/charts/options/common.ts:51-64 📄 openmetadata-ui-core-components/src/main/resources/ui/src/charts/options/common.ts:67-77

ChartSeries.color accepts any string, but areaGradient sends it through hexToRgba, which only understands #RGB/#RRGGBB. Any other value, such as rgb(...), hsl(...) or a named colour, gets parsed as hex. For example, 'red' becomes 'rreedd' and gives rgba(0, 238, 221, 0.2), while rgb(...) falls back to black. So an area chart with such a colour draws a fill that doesn't match its line. Fix: return non-hex input unchanged and use opacity on areaStyle, or validate the input with a /^#([0-9a-f]{3}|[0-9a-f]{6})$/i check and fall back to the raw colour plus areaStyle.opacity.

Only convert real hex values; leave other colours untouched
export const hexToRgba = (hex: string, alpha: number): string => {
  if (!/^#([0-9a-f]{3}|[0-9a-f]{6})$/i.test(hex)) {
    return hex;
  }
  const clean = hex.slice(1);
  const full = clean.length === 3 ? clean.replace(/./g, (c) => c + c) : clean;
  const channel = (start: number) =>
    Number.parseInt(full.slice(start, start + 2), 16);

  return `rgba(${channel(0)}, ${channel(2)}, ${channel(4)}, ${alpha})`;
};
🤖 Prompt for agents
Code Review: Introduces a comprehensive ECharts-based chart module with typed components, theme support, and server-side rendering compatibility. Two minor issues to address: tooltip formatter and other top-level option overrides linger after re-render because missing keys are not explicitly set to `undefined`, and `hexToRgba` produces incorrect area fills for non-hex colour values like `rgb(...)` or named colours.

1. 💡 Bug: Removed tooltip formatter/option overrides linger after re-render
   Files: openmetadata-ui-core-components/src/main/resources/ui/src/charts/options/common.ts:92-93, openmetadata-ui-core-components/src/main/resources/ui/src/charts/options/merge.ts:42-48

   `EChart` updates with `notMerge: false`, and only `series`, `xAxis`, `yAxis`, `grid` and `dataZoom` are in `replaceMerge`. `tooltipConfig` adds `formatter` through a conditional spread (`...(props.formatter ? { formatter } : {})`), so the key is simply missing when a caller drops `tooltip.formatter`. ECharts' component merge leaves missing keys alone, so the old top-level formatter keeps rendering. The PR says removed tooltip formatters are cleared, but the integration test only covers the per-series `valueFormatter`. The same happens to any `tooltip`/`legend`/`aria` keys a caller later removes from the `option` override. Fix: always write the key (`formatter: props.formatter`). ECharts merges with overwrite, so an explicit `undefined` clears the old value, which is the same approach `areaStyle`/series `tooltip` already use. Optionally, add `tooltip` to `REPLACE_MERGE_KEYS`.

   Fix (Always set formatter so a removal clears it on merge):
     valueFormatter: (value) => formatTooltipValue(value),
     // Always set, so a re-render that drops the formatter clears the old one.
     formatter: props.formatter,
   });

2. 💡 Edge Case: hexToRgba produces wrong area fill for non-hex series colours
   Files: openmetadata-ui-core-components/src/main/resources/ui/src/charts/options/common.ts:51-64, openmetadata-ui-core-components/src/main/resources/ui/src/charts/options/common.ts:67-77

   `ChartSeries.color` accepts any string, but `areaGradient` sends it through `hexToRgba`, which only understands `#RGB`/`#RRGGBB`. Any other value, such as `rgb(...)`, `hsl(...)` or a named colour, gets parsed as hex. For example, `'red'` becomes `'rreedd'` and gives `rgba(0, 238, 221, 0.2)`, while `rgb(...)` falls back to black. So an area chart with such a colour draws a fill that doesn't match its line. Fix: return non-hex input unchanged and use `opacity` on `areaStyle`, or validate the input with a `/^#([0-9a-f]{3}|[0-9a-f]{6})$/i` check and fall back to the raw colour plus `areaStyle.opacity`.

   Fix (Only convert real hex values; leave other colours untouched):
   export const hexToRgba = (hex: string, alpha: number): string => {
     if (!/^#([0-9a-f]{3}|[0-9a-f]{6})$/i.test(hex)) {
       return hex;
     }
     const clean = hex.slice(1);
     const full = clean.length === 3 ? clean.replace(/./g, (c) => c + c) : clean;
     const channel = (start: number) =>
       Number.parseInt(full.slice(start, start + 2), 16);
   
     return `rgba(${channel(0)}, ${channel(2)}, ${channel(4)}, ${alpha})`;
   };

Review coverage

📋 Rules No rules evaluated

🧪 Functional validation Not enabled · Set up

🤖 Auto-approval Not enabled · Set up

Options

Display: compact → Counting what did not apply, without listing it.

Comment with these commands to change the behavior for this request:

Compact
gitar display:verbose         

Was this helpful? React with 👍 / 👎 | Powered by Gitar — free for open source

@sonarqubecloud

Copy link
Copy Markdown

This branch was previously deployed

1 inactive deployment
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

safe to test Add this label to run secure Github workflows on PRs UI UI specific issues

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants