From 23d21923f74a51cde1ceb6923293139f0c53daaa Mon Sep 17 00:00:00 2001 From: YoussefMohamed2k19 Date: Mon, 3 Aug 2026 13:26:11 +0300 Subject: [PATCH 1/4] fix(recharts): stop routing component props to wrapperStyle Declare stroke_dasharray on ReferenceLine and tick_formatter on Axis (shared by XAxis/YAxis) as explicit fields. Previously undeclared kwargs fell through Component.create()'s default classification into style, which _get_style() (added in #4447) then dumps into wrapperStyle, so the props never reached the underlying Recharts component. Fixes #6575 --- news/6575.bugfix.md | 1 + .../src/reflex_components_recharts/cartesian.py | 8 ++++++++ pyi_hashes.json | 2 +- .../units/components/recharts/test_cartesian.py | 17 +++++++++++++++++ 4 files changed, 27 insertions(+), 1 deletion(-) create mode 100644 news/6575.bugfix.md diff --git a/news/6575.bugfix.md b/news/6575.bugfix.md new file mode 100644 index 00000000000..178127bedcb --- /dev/null +++ b/news/6575.bugfix.md @@ -0,0 +1 @@ +Fix Recharts component props (e.g. `stroke_dasharray` on `reference_line`, `tick_formatter` on `x_axis`/`y_axis`) being misclassified as CSS and routed to `wrapperStyle` instead of reaching the underlying Recharts component. diff --git a/packages/reflex-components-recharts/src/reflex_components_recharts/cartesian.py b/packages/reflex-components-recharts/src/reflex_components_recharts/cartesian.py index 712623b4d51..2526426c9ed 100644 --- a/packages/reflex-components-recharts/src/reflex_components_recharts/cartesian.py +++ b/packages/reflex-components-recharts/src/reflex_components_recharts/cartesian.py @@ -114,6 +114,10 @@ class Axis(Recharts): tick_size: Var[int] = field(doc="The length of tick line. Default: 6") + tick_formatter: Var[str] = field( + doc="A function to format the tick value shown in the axis." + ) + min_tick_gap: Var[int] = field( doc="The minimum gap between two adjacent labels. Default: 5" ) @@ -816,6 +820,10 @@ class ReferenceLine(Reference): doc="The width of the stroke. Default: 1" ) + stroke_dasharray: Var[str] = field( + doc="The pattern of dashes and gaps used to paint the reference line." + ) + # Valid children components _valid_children: ClassVar[list[str]] = ["Label"] diff --git a/pyi_hashes.json b/pyi_hashes.json index 144cc2dfcf4..a117cb56a72 100644 --- a/pyi_hashes.json +++ b/pyi_hashes.json @@ -112,7 +112,7 @@ "packages/reflex-components-react-player/src/reflex_components_react_player/react_player.pyi": "86fc106181638c6a0a2a199332be817f", "packages/reflex-components-react-player/src/reflex_components_react_player/video.pyi": "2682dc6e825d25307d8390d01e2bd653", "packages/reflex-components-recharts/src/reflex_components_recharts/__init__.pyi": "359e123d9a046557ce05a96ce10313f5", - "packages/reflex-components-recharts/src/reflex_components_recharts/cartesian.pyi": "3485aaabbfd3ca89908ae19425c7297e", + "packages/reflex-components-recharts/src/reflex_components_recharts/cartesian.pyi": "541710a9f3f7a3ad3f5539f1f7f856dc", "packages/reflex-components-recharts/src/reflex_components_recharts/charts.pyi": "280a7cd51298ee676f3d076104133f44", "packages/reflex-components-recharts/src/reflex_components_recharts/general.pyi": "f5e3491c4e1f69ba89085682888888a9", "packages/reflex-components-recharts/src/reflex_components_recharts/polar.pyi": "99ebcfc07868061bdc3c2010d85a153f", diff --git a/tests/units/components/recharts/test_cartesian.py b/tests/units/components/recharts/test_cartesian.py index 078455f4411..fa478e24c98 100644 --- a/tests/units/components/recharts/test_cartesian.py +++ b/tests/units/components/recharts/test_cartesian.py @@ -3,6 +3,7 @@ Bar, Brush, Line, + ReferenceLine, Scatter, XAxis, YAxis, @@ -45,6 +46,22 @@ def test_line(): assert line["name"] == "RechartsLine" +def test_reference_line_stroke_dasharray(): + reference_line = ReferenceLine.create(stroke_dasharray="8 8") + assert "strokeDasharray" not in reference_line.style + props = reference_line.render()["props"] + assert any("strokeDasharray" in prop for prop in props) + assert not any("wrapperStyle" in prop for prop in props) + + +def test_xaxis_tick_formatter(): + x_axis = XAxis.create(tick_formatter="(value) => value") + assert "tickFormatter" not in x_axis.style + props = x_axis.render()["props"] + assert any("tickFormatter" in prop for prop in props) + assert not any("wrapperStyle" in prop for prop in props) + + def test_scatter(): scatter = Scatter.create().render() assert scatter["name"] == "RechartsScatter" From b2b834c5ebb24bc50228263782baa2fd582ab619 Mon Sep 17 00:00:00 2001 From: YoussefMohamed2k19 Date: Mon, 3 Aug 2026 13:54:10 +0300 Subject: [PATCH 2/4] fix(recharts): make tick_formatter a real JS function, not a quoted string Addresses review feedback on #6833: - tick_formatter was declared Var[str], so a plain Python string got wrapped by LiteralVar into a JSON-quoted string literal. Recharts received "(value) => value" as text, not a callable, so the formatter was silently never invoked. Axis.create() now wraps a str value in FunctionStringVar so it renders as raw, unquoted JS. - Tests previously only checked that the prop key existed, which would still pass for an empty/dropped value. They now assert the exact rendered prop string, and cover YAxis (inherits from Axis same as XAxis) in addition to XAxis. --- .../reflex_components_recharts/cartesian.py | 22 +++++++++++++++++-- pyi_hashes.json | 2 +- .../components/recharts/test_cartesian.py | 14 +++++++++--- 3 files changed, 32 insertions(+), 6 deletions(-) diff --git a/packages/reflex-components-recharts/src/reflex_components_recharts/cartesian.py b/packages/reflex-components-recharts/src/reflex_components_recharts/cartesian.py index 2526426c9ed..a8c85041e5b 100644 --- a/packages/reflex-components-recharts/src/reflex_components_recharts/cartesian.py +++ b/packages/reflex-components-recharts/src/reflex_components_recharts/cartesian.py @@ -10,6 +10,7 @@ from reflex_base.constants.colors import Color from reflex_base.event import EventHandler, no_args_event_spec from reflex_base.vars.base import LiteralVar, Var +from reflex_base.vars.function import FunctionStringVar from .recharts import ( ACTIVE_DOT_TYPE, @@ -114,14 +115,31 @@ class Axis(Recharts): tick_size: Var[int] = field(doc="The length of tick line. Default: 6") - tick_formatter: Var[str] = field( - doc="A function to format the tick value shown in the axis." + tick_formatter: Var[Any] = field( + doc="A function to format the tick value shown in the axis. Pass a " + "raw JS function body as a string, e.g. tick_formatter=" + '"(value) => value.toFixed(2)".' ) min_tick_gap: Var[int] = field( doc="The minimum gap between two adjacent labels. Default: 5" ) + @classmethod + def create(cls, *children, **props): + """Create an Axis component. + + Args: + *children: The children of the component. + **props: The properties of the component. + + Returns: + The Axis component. + """ + if isinstance(tick_formatter := props.get("tick_formatter"), str): + props["tick_formatter"] = FunctionStringVar.create(tick_formatter) + return super().create(*children, **props) + stroke: Var[str | Color] = field( default=LiteralVar.create(Color("gray", 9)), doc='The stroke color of axis. Default: rx.color("gray", 9)', diff --git a/pyi_hashes.json b/pyi_hashes.json index a117cb56a72..2733f4ccc2e 100644 --- a/pyi_hashes.json +++ b/pyi_hashes.json @@ -112,7 +112,7 @@ "packages/reflex-components-react-player/src/reflex_components_react_player/react_player.pyi": "86fc106181638c6a0a2a199332be817f", "packages/reflex-components-react-player/src/reflex_components_react_player/video.pyi": "2682dc6e825d25307d8390d01e2bd653", "packages/reflex-components-recharts/src/reflex_components_recharts/__init__.pyi": "359e123d9a046557ce05a96ce10313f5", - "packages/reflex-components-recharts/src/reflex_components_recharts/cartesian.pyi": "541710a9f3f7a3ad3f5539f1f7f856dc", + "packages/reflex-components-recharts/src/reflex_components_recharts/cartesian.pyi": "de962ad6968036849f605e473acd42b0", "packages/reflex-components-recharts/src/reflex_components_recharts/charts.pyi": "280a7cd51298ee676f3d076104133f44", "packages/reflex-components-recharts/src/reflex_components_recharts/general.pyi": "f5e3491c4e1f69ba89085682888888a9", "packages/reflex-components-recharts/src/reflex_components_recharts/polar.pyi": "99ebcfc07868061bdc3c2010d85a153f", diff --git a/tests/units/components/recharts/test_cartesian.py b/tests/units/components/recharts/test_cartesian.py index fa478e24c98..d39290e7c27 100644 --- a/tests/units/components/recharts/test_cartesian.py +++ b/tests/units/components/recharts/test_cartesian.py @@ -50,15 +50,23 @@ def test_reference_line_stroke_dasharray(): reference_line = ReferenceLine.create(stroke_dasharray="8 8") assert "strokeDasharray" not in reference_line.style props = reference_line.render()["props"] - assert any("strokeDasharray" in prop for prop in props) + assert 'strokeDasharray:"8 8"' in props assert not any("wrapperStyle" in prop for prop in props) def test_xaxis_tick_formatter(): - x_axis = XAxis.create(tick_formatter="(value) => value") + x_axis = XAxis.create(tick_formatter="(value) => value.toFixed(2)") assert "tickFormatter" not in x_axis.style props = x_axis.render()["props"] - assert any("tickFormatter" in prop for prop in props) + assert "tickFormatter:(value) => value.toFixed(2)" in props + assert not any("wrapperStyle" in prop for prop in props) + + +def test_yaxis_tick_formatter(): + y_axis = YAxis.create(tick_formatter="(value) => value.toFixed(2)") + assert "tickFormatter" not in y_axis.style + props = y_axis.render()["props"] + assert "tickFormatter:(value) => value.toFixed(2)" in props assert not any("wrapperStyle" in prop for prop in props) From 4c9fa1050e9e886ec9911e818b19ad9bbaa15f11 Mon Sep 17 00:00:00 2001 From: YoussefMohamed2k19 Date: Mon, 3 Aug 2026 14:11:01 +0300 Subject: [PATCH 3/4] fix(recharts): restore type safety on tick_formatter, fix doc wording MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses two more review comments on #6833: - tick_formatter was Var[Any] after the previous fix, silently accepting any type (int, list, ...) and only failing at the Recharts/JS layer with a garbled prop. Narrowed to Var[str | Callable[..., Any]] so non-callable, non-string values are rejected with a TypeError at component-creation time, both at runtime and in the generated .pyi stubs. (A first attempt using Var[FunctionVar]/Var[ReflexCallable[Any, Any]] hit a framework quirk where typehint_issubclass compares two independently constructed Protocol generic aliases by identity rather than structural equality, and intermittently rejected the exact value it just created — collections.abc.Callable doesn't hit that path.) - Doc string said "raw JS function body" but the example (and the actual behavior) is a full function expression, e.g. "(value) => value.toFixed(2)", not just a body like "return value". Reworded to "function expression" to match. --- .../src/reflex_components_recharts/cartesian.py | 11 +++++++---- pyi_hashes.json | 2 +- tests/units/components/recharts/test_cartesian.py | 6 ++++++ 3 files changed, 14 insertions(+), 5 deletions(-) diff --git a/packages/reflex-components-recharts/src/reflex_components_recharts/cartesian.py b/packages/reflex-components-recharts/src/reflex_components_recharts/cartesian.py index a8c85041e5b..c44fe90c6b3 100644 --- a/packages/reflex-components-recharts/src/reflex_components_recharts/cartesian.py +++ b/packages/reflex-components-recharts/src/reflex_components_recharts/cartesian.py @@ -2,7 +2,7 @@ from __future__ import annotations -from collections.abc import Sequence +from collections.abc import Callable, Sequence from typing import Any, ClassVar, TypedDict from reflex_base.components.component import field @@ -115,9 +115,9 @@ class Axis(Recharts): tick_size: Var[int] = field(doc="The length of tick line. Default: 6") - tick_formatter: Var[Any] = field( + tick_formatter: Var[str | Callable[..., Any]] = field( doc="A function to format the tick value shown in the axis. Pass a " - "raw JS function body as a string, e.g. tick_formatter=" + "raw JS function expression as a string, e.g. tick_formatter=" '"(value) => value.toFixed(2)".' ) @@ -137,7 +137,10 @@ def create(cls, *children, **props): The Axis component. """ if isinstance(tick_formatter := props.get("tick_formatter"), str): - props["tick_formatter"] = FunctionStringVar.create(tick_formatter) + props["tick_formatter"] = FunctionStringVar.create( + tick_formatter, + _var_type=Callable[..., Any], # pyright: ignore [reportArgumentType] + ) return super().create(*children, **props) stroke: Var[str | Color] = field( diff --git a/pyi_hashes.json b/pyi_hashes.json index 2733f4ccc2e..78c243d9e27 100644 --- a/pyi_hashes.json +++ b/pyi_hashes.json @@ -112,7 +112,7 @@ "packages/reflex-components-react-player/src/reflex_components_react_player/react_player.pyi": "86fc106181638c6a0a2a199332be817f", "packages/reflex-components-react-player/src/reflex_components_react_player/video.pyi": "2682dc6e825d25307d8390d01e2bd653", "packages/reflex-components-recharts/src/reflex_components_recharts/__init__.pyi": "359e123d9a046557ce05a96ce10313f5", - "packages/reflex-components-recharts/src/reflex_components_recharts/cartesian.pyi": "de962ad6968036849f605e473acd42b0", + "packages/reflex-components-recharts/src/reflex_components_recharts/cartesian.pyi": "1eedb94040e51bd722030b6e57815ce1", "packages/reflex-components-recharts/src/reflex_components_recharts/charts.pyi": "280a7cd51298ee676f3d076104133f44", "packages/reflex-components-recharts/src/reflex_components_recharts/general.pyi": "f5e3491c4e1f69ba89085682888888a9", "packages/reflex-components-recharts/src/reflex_components_recharts/polar.pyi": "99ebcfc07868061bdc3c2010d85a153f", diff --git a/tests/units/components/recharts/test_cartesian.py b/tests/units/components/recharts/test_cartesian.py index d39290e7c27..9e644e36cc6 100644 --- a/tests/units/components/recharts/test_cartesian.py +++ b/tests/units/components/recharts/test_cartesian.py @@ -1,3 +1,4 @@ +import pytest from reflex_components_recharts import ( Area, Bar, @@ -70,6 +71,11 @@ def test_yaxis_tick_formatter(): assert not any("wrapperStyle" in prop for prop in props) +def test_xaxis_tick_formatter_rejects_non_callable(): + with pytest.raises(TypeError): + XAxis.create(tick_formatter=123) # pyright: ignore [reportArgumentType] + + def test_scatter(): scatter = Scatter.create().render() assert scatter["name"] == "RechartsScatter" From 65083f476a4470f9ee2b1ee68f8f6b1c7e581b57 Mon Sep 17 00:00:00 2001 From: YoussefMohamed2k19 Date: Mon, 3 Aug 2026 14:25:02 +0300 Subject: [PATCH 4/4] fix(recharts): reject plain Python callables for tick_formatter Addresses review feedback on #6833: tick_formatter=lambda value: value passed the declared/runtime type check (a lambda IS a collections.abc.Callable instance), but only strings were converted to a JS-function Var in create(). The raw Python lambda sat unconverted on the component, then blew up at render() with a cryptic "Unsupported type for LiteralVar" error instead of a clear message at creation time. create() now explicitly rejects any non-str, non-Var tick_formatter (covering lambdas, named functions, and other Python objects) with a TypeError up front. Also normalizes an already-Var-wrapped FunctionVar's _var_type so passing e.g. FunctionStringVar.create("someGlobalFn") directly still works regardless of how the caller built it. --- .../reflex_components_recharts/cartesian.py | 29 +++++++++++++++---- .../components/recharts/test_cartesian.py | 14 +++++++++ 2 files changed, 37 insertions(+), 6 deletions(-) diff --git a/packages/reflex-components-recharts/src/reflex_components_recharts/cartesian.py b/packages/reflex-components-recharts/src/reflex_components_recharts/cartesian.py index c44fe90c6b3..bf1d32cd8f5 100644 --- a/packages/reflex-components-recharts/src/reflex_components_recharts/cartesian.py +++ b/packages/reflex-components-recharts/src/reflex_components_recharts/cartesian.py @@ -10,7 +10,7 @@ from reflex_base.constants.colors import Color from reflex_base.event import EventHandler, no_args_event_spec from reflex_base.vars.base import LiteralVar, Var -from reflex_base.vars.function import FunctionStringVar +from reflex_base.vars.function import FunctionStringVar, FunctionVar from .recharts import ( ACTIVE_DOT_TYPE, @@ -136,11 +136,28 @@ def create(cls, *children, **props): Returns: The Axis component. """ - if isinstance(tick_formatter := props.get("tick_formatter"), str): - props["tick_formatter"] = FunctionStringVar.create( - tick_formatter, - _var_type=Callable[..., Any], # pyright: ignore [reportArgumentType] - ) + if (tick_formatter := props.get("tick_formatter")) is not None: + if isinstance(tick_formatter, str): + props["tick_formatter"] = FunctionStringVar.create( + tick_formatter, + _var_type=Callable[..., Any], # pyright: ignore [reportArgumentType] + ) + elif isinstance(tick_formatter, FunctionVar): + # Normalize to a consistent _var_type regardless of how the + # caller constructed the FunctionVar, so it satisfies the + # declared field type below. + props["tick_formatter"] = tick_formatter._replace( + _var_type=Callable[..., Any] # pyright: ignore [reportArgumentType] + ) + elif not isinstance(tick_formatter, Var): + msg = ( + "tick_formatter must be a raw JS function expression string " + f'(e.g. "(value) => value.toFixed(2)") or a Var, got a Python ' + f"{type(tick_formatter).__name__}. Python values (including " + "plain callables like lambdas) cannot be sent to the client " + "as-is and are not supported." + ) + raise TypeError(msg) return super().create(*children, **props) stroke: Var[str | Color] = field( diff --git a/tests/units/components/recharts/test_cartesian.py b/tests/units/components/recharts/test_cartesian.py index 9e644e36cc6..52d36e7543e 100644 --- a/tests/units/components/recharts/test_cartesian.py +++ b/tests/units/components/recharts/test_cartesian.py @@ -1,4 +1,5 @@ import pytest +from reflex_base.vars.function import FunctionStringVar from reflex_components_recharts import ( Area, Bar, @@ -76,6 +77,19 @@ def test_xaxis_tick_formatter_rejects_non_callable(): XAxis.create(tick_formatter=123) # pyright: ignore [reportArgumentType] +def test_xaxis_tick_formatter_rejects_python_callable(): + with pytest.raises(TypeError, match="Python"): + XAxis.create( + tick_formatter=lambda value: value # pyright: ignore [reportArgumentType] + ) + + +def test_xaxis_tick_formatter_accepts_prebuilt_function_var(): + x_axis = XAxis.create(tick_formatter=FunctionStringVar.create("myGlobalFormatter")) + props = x_axis.render()["props"] + assert "tickFormatter:myGlobalFormatter" in props + + def test_scatter(): scatter = Scatter.create().render() assert scatter["name"] == "RechartsScatter"