Skip to content

Charts: Fix conversion funnel chart crash on non-hex colors#47786

Closed
kangzj wants to merge 4 commits intotrunkfrom
fix/CHARTS-196-funnel-chart-non-hex-colors
Closed

Charts: Fix conversion funnel chart crash on non-hex colors#47786
kangzj wants to merge 4 commits intotrunkfrom
fix/CHARTS-196-funnel-chart-non-hex-colors

Conversation

@kangzj
Copy link
Copy Markdown
Contributor

@kangzj kangzj commented Mar 25, 2026

Fixes https://linear.app/a8c/issue/CHARTS-196/find-32-conversion-funnel-chart-crashes-on-non-hex-colors

Proposed changes

  • Add colorToRgba() utility that accepts any valid CSS color format (hex, rgb, hsl, named colors) and returns null for unparseable values instead of throwing
  • Replace hexToRgba() with colorToRgba() in ConversionFunnelChart to prevent crashes when themes provide non-hex color values (e.g., rgb(...), hsl(...), named colors)

Other information

  • Generate changelog entries for this PR (using AI).

Related product discussion/links

Does this pull request change what data or activity we track or use?

No.

Testing instructions

  • Render a ConversionFunnelChart inside a GlobalChartsProvider with a non-hex primary color (e.g., rgb(255, 0, 0), hsl(0, 100%, 50%), or red)
  • Before: Chart crashes with Error: Hex color must start with #
  • After: Chart renders correctly, applying the color with 8% opacity for the bar background

Add colorToRgba() utility that gracefully handles any valid CSS color
format (hex, rgb, hsl, named colors) and returns null for unparseable
values instead of throwing.

Replace hexToRgba() with colorToRgba() in ConversionFunnelChart to
prevent crashes when themes provide non-hex color values.

Fixes CHARTS-196
@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Mar 25, 2026

Are you an Automattician? Please test your changes on all WordPress.com environments to help mitigate accidental explosions.

  • To test on WoA, go to the Plugins menu on a WoA dev site. Click on the "Upload" button and follow the upgrade flow to be able to upload, install, and activate the Jetpack Beta plugin. Once the plugin is active, go to Jetpack > Jetpack Beta, select your plugin (Jetpack), and enable the fix/CHARTS-196-funnel-chart-non-hex-colors branch.
  • To test on Simple, run the following command on your sandbox:
bin/jetpack-downloader test jetpack fix/CHARTS-196-funnel-chart-non-hex-colors

Interested in more tips and information?

  • In your local development environment, use the jetpack rsync command to sync your changes to a WoA dev blog.
  • Read more about our development workflow here: PCYsg-eg0-p2
  • Figure out when your changes will be shipped to customers here: PCYsg-eg5-p2

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Mar 25, 2026

Thank you for your PR!

When contributing to Jetpack, we have a few suggestions that can help us test and review your patch:

  • ✅ Include a description of your PR changes.
  • ✅ Add a "[Status]" label (In Progress, Needs Review, ...).
  • ✅ Add testing instructions.
  • ✅ Specify whether this PR includes any changes to data or privacy.
  • ✅ Add changelog entries to affected projects

This comment will be updated as you work on your PR and make changes. If you think that some of those checks are not needed for your PR, please explain why you think so. Thanks for cooperation 🤖


Follow this PR Review Process:

  1. Ensure all required checks appearing at the bottom of this PR are passing.
  2. Make sure to test your changes on all platforms that it applies to. You're responsible for the quality of the code you ship.
  3. You can use GitHub's Reviewers functionality to request a review.
  4. When it's reviewed and merged, you will be pinged in Slack to deploy the changes to WordPress.com simple once the build is done.

If you have questions about anything, reach out in #jetpack-developers for guidance!

@github-actions github-actions bot added the [Status] Needs Author Reply We need more details from you. This label will be auto-added until the PR meets all requirements. label Mar 25, 2026
@jp-launch-control
Copy link
Copy Markdown

jp-launch-control bot commented Mar 25, 2026

Code Coverage Summary

Coverage changed in 1 file.

File Coverage Δ% Δ Uncovered
projects/js-packages/charts/src/utils/color-utils.ts 82/84 (97.62%) 0.22% 0 💚

Full summary · PHP report · JS report

@kangzj kangzj added [Status] Needs Team Review Obsolete. Use Needs Review instead. and removed [Status] Needs Author Reply We need more details from you. This label will be auto-added until the PR meets all requirements. [Status] In Progress labels Mar 26, 2026
kangzj added 2 commits March 26, 2026 14:19
hexToRgba was only used in the conversion funnel chart (now migrated)
and a leaderboard storybook story. colorToRgba is a strict superset
that handles any CSS color format, making the hex-only variant dead code.
@kangzj kangzj marked this pull request as ready for review March 26, 2026 01:37
@kangzj kangzj requested review from a team and adamwoodnz March 26, 2026 01:37
@kangzj kangzj self-assigned this Mar 26, 2026
* @param alpha - The opacity value. Values outside the [0, 1] range will be clamped by d3.
* @return The rgba color string (e.g., 'rgba(255, 0, 0, 0.5)'), or null if the color is invalid
*/
export const hexToRgba = ( hex: string, alpha: number ): string => {
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.

Since we have exported hexToRgba as part of the public API, should we keep it and mark it as deprecated, or treat this as a breaking change in the changelog?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Kept hexToRgba as a deprecated wrapper that delegates to colorToRgba, preserving the original throwing behavior for backward compatibility. Added tests for the deprecated function. Resolved in bc3ac3c.

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.

We are past 1.0 now so I think we need to deprecate, and remove in 2.0

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Agreed and addressed

@adamwoodnz
Copy link
Copy Markdown
Contributor

@claude review please

@claude
Copy link
Copy Markdown

claude bot commented Mar 26, 2026

Claude finished @adamwoodnz's task in 1m 29s —— View job


Code Review

  • Gather context
  • Read changed files
  • Provide review feedback

Overall this is a clean, well-targeted fix. The approach of using d3-color as the parsing backend is solid — it handles the full CSS color gamut correctly. A few items worth discussing:


colorToRgba return format inconsistency with alpha=1

projects/js-packages/charts/src/utils/color-utils.ts:54

return parsed.copy( { opacity: alpha } ).formatRgb();

d3-color's formatRgb() returns "rgb(...)" (not "rgba(...)") when opacity is exactly 1. So:

colorToRgba('#ff0000', 1)  // → "rgb(255, 0, 0)", not "rgba(255, 0, 0, 1)"
colorToRgba('#ff0000', 0.5) // → "rgba(255, 0, 0, 0.5)" ✓

This is inconsistent with the function name and JSDoc (@return The rgba color string). It's not a bug for the current call site (alpha=0.08), but a consumer who passes alpha: 1 expecting rgba(...) output would get a silent surprise. The test suite has no coverage for alpha=0 or alpha=1 edge cases.

Minor fix options: use formatRgb() but document the behavior, or force rgba output via string formatting. At minimum, a test case for alpha=1 would document the behavior explicitly. Fix this →


hexToRgba deprecation not reflected in changelog

The changelog entry covers the bug fix, but hexToRgba is now deprecated as a public API. Since this package is past 1.0, a separate deprecated changelog entry for hexToRgba would help consumers know what to migrate away from. The AGENTS.md notes backward compatibility is expected for public APIs.


hexToRgba can still crash when colorToRgba returns null unexpectedly

projects/js-packages/charts/src/utils/color-utils.ts:73

return colorToRgba( hex, alpha ) as string;

After validateHexColor(hex) and the alpha number check, colorToRgba should never return null here (valid hex + valid number). The as string cast is technically correct. However, if d3-color ever fails to parse something that passed hex validation (unlikely, but defensive), the cast would produce a runtime null typed as string with no error. A tighter option: return colorToRgba( hex, alpha ) ?? '' or an explicit assertion. Low priority.


Leaderboard story import update

projects/js-packages/charts/src/charts/leaderboard-chart/stories/index.stories.tsx

The import swap from hexToRgba to colorToRgba is correct and appropriate since the story would otherwise be using the deprecated function.


What's done well

  • The core colorToRgba design is clean — delegates to d3-color, returns null for invalid input, no throwing, well-documented.
  • The fallback colorToRgba(barColor, 0.08) || 'rgba(0, 0, 0, 0.08)' in the chart component handles the null case correctly.
  • Deprecated hexToRgba wrapper preserves the original throwing behavior for backward compatibility.
  • Test coverage for the new function is solid across hex, named, rgb, hsl, and error cases.
  • Changelog entry is present and correctly typed.

Summary: The bug fix itself is correct and ready. The main actionable items are: (1) add a deprecated changelog entry for hexToRgba, and (2) add test coverage for alpha=1 to document the rgb() vs rgba() output behavior.

@adamwoodnz
Copy link
Copy Markdown
Contributor

@claude I'm curious - this seems like a valid defensive improvement, but is it actually possible for this bug to occur? It appears the provider pipeline prevents non hex colors reaching hexToRgba.

@claude
Copy link
Copy Markdown

claude bot commented Mar 26, 2026

Claude finished @adamwoodnz's task in 2m 39s —— View job


Analysis: Can non-hex colors reach hexToRgba?

  • Gather context
  • Trace color flow through provider pipeline
  • Determine if non-hex colors can reach hexToRgba
  • Post findings

Short answer: The pipeline does partially protect against this, but has a gap specifically for CSS named colors (e.g. red, blue). The fix is valid but the triggerable scenario is narrower than the PR description implies.

The pipeline path

When primaryColor is set in the theme, it goes through resolveColornormalizeColorToHex before reaching colorToRgba. Here's what normalizeColorToHex handles:

Input Handled? Output
#ff0000 (hex) #ff0000
#f00 (3-digit hex) #ff0000
rgb(255, 0, 0) ✅ → d3-color → formatHex #ff0000
hsl(0, 100%, 50%) ✅ → d3-color → formatHex #ff0000
var(--my-color) ✅ → resolved → recurse hex
red (named color) ❌ falls through to return color red

So named CSS colors are the gapnormalizeColorToHex has no branch for them and returns them as-is. The examples in the PR description (rgb(...), hsl(...)) are actually already normalized to hex by the pipeline and wouldn't have crashed with the old code.

A second protection layer

Even with a named primaryColor, the old code only crashed in hexToRgba when computing barBackgroundColor:

// global-charts-provider.tsx:249
const barBackgroundColor =
    backgroundColor || colorToRgba( barColor, 0.08 ) || 'rgba(0, 0, 0, 0.08)';

The default theme provides backgroundColor: '#F3F4F6', which is always merged in by mergeThemes/deepmerge. So the colorToRgba(barColor, 0.08) branch is only reached if a consumer explicitly overrides backgroundColor to a falsy value ('', or similar).

The triggerable scenario

All three of these must be true simultaneously:

  1. Consumer provides primaryColor as a named CSS color (e.g. 'red') in the theme's conversionFunnelChart config
  2. Consumer explicitly sets backgroundColor: '' (or otherwise falsy) in the same config
  3. No GlobalChartsProvider exists, or the existing provider doesn't normalize named colors

This is a fairly specific combination. It's unlikely to occur in practice today, but it's a legitimate edge case that's worth guarding against defensively — particularly given normalizeColorToHex has a known blind spot for named colors.

The fix is correct. colorToRgba handles named colors via d3-color and returns null on failure, which the fallback || 'rgba(0, 0, 0, 0.08)' covers. Whether it's worth also fixing normalizeColorToHex to handle named colors (closing the gap at source) is a separate conversation.


@kangzj kangzj marked this pull request as draft March 26, 2026 22:01
@adamwoodnz
Copy link
Copy Markdown
Contributor

Whether it's worth also fixing normalizeColorToHex to handle named colors (closing the gap at source) is a separate conversation.

FYI I've refreshed #46349 which adds support for various color format including named colors, so that would cover the edge case identified.

@kangzj kangzj closed this Mar 27, 2026
@github-actions github-actions bot removed [Status] In Progress [Status] Needs Team Review Obsolete. Use Needs Review instead. labels Mar 27, 2026
@adamwoodnz adamwoodnz deleted the fix/CHARTS-196-funnel-chart-non-hex-colors branch March 29, 2026 23:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants