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..bf1d32cd8f5 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 @@ -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, FunctionVar from .recharts import ( ACTIVE_DOT_TYPE, @@ -114,10 +115,51 @@ class Axis(Recharts): tick_size: Var[int] = field(doc="The length of tick line. Default: 6") + tick_formatter: Var[str | Callable[..., Any]] = field( + doc="A function to format the tick value shown in the axis. Pass a " + "raw JS function expression 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 (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( default=LiteralVar.create(Color("gray", 9)), doc='The stroke color of axis. Default: rx.color("gray", 9)', @@ -816,6 +858,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..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": "3485aaabbfd3ca89908ae19425c7297e", + "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 078455f4411..52d36e7543e 100644 --- a/tests/units/components/recharts/test_cartesian.py +++ b/tests/units/components/recharts/test_cartesian.py @@ -1,8 +1,11 @@ +import pytest +from reflex_base.vars.function import FunctionStringVar from reflex_components_recharts import ( Area, Bar, Brush, Line, + ReferenceLine, Scatter, XAxis, YAxis, @@ -45,6 +48,48 @@ 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 '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.toFixed(2)") + assert "tickFormatter" not in x_axis.style + props = x_axis.render()["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) + + +def test_xaxis_tick_formatter_rejects_non_callable(): + with pytest.raises(TypeError): + 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"