Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions news/6575.bugfix.md
Original file line number Diff line number Diff line change
@@ -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.
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,15 @@

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
from reflex_base.constants import EventTriggers
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,
Expand Down Expand Up @@ -114,10 +115,51 @@ class Axis(Recharts):

tick_size: Var[int] = field(doc="The length of tick line. Default: 6")

Comment thread
greptile-apps[bot] marked this conversation as resolved.
tick_formatter: Var[str | Callable[..., Any]] = field(
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
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)".'
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
)

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):

@cubic-dev-ai cubic-dev-ai Bot Aug 3, 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.

P2: A reactive string such as State.formatter still reaches Recharts as tickFormatter={state.formatter}, where Recharts expects a function. The Var branch should validate a callable _var_type instead of allowing every Var.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/reflex-components-recharts/src/reflex_components_recharts/cartesian.py, line 152:

<comment>A reactive string such as `State.formatter` still reaches Recharts as `tickFormatter={state.formatter}`, where Recharts expects a function. The `Var` branch should validate a callable `_var_type` instead of allowing every `Var`.</comment>

<file context>
@@ -136,11 +136,28 @@ def create(cls, *children, **props):
+                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 "
</file context>
Fix with cubic

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 "

@cubic-dev-ai cubic-dev-ai Bot Aug 3, 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.

P3: Type checkers still accept the Python callables this branch now rejects at runtime because the generated create signature includes bare Callable. An explicit typed tick_formatter parameter/overload should expose only raw str and function-valued Var inputs.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/reflex-components-recharts/src/reflex_components_recharts/cartesian.py, line 157:

<comment>Type checkers still accept the Python callables this branch now rejects at runtime because the generated `create` signature includes bare `Callable`. An explicit typed `tick_formatter` parameter/overload should expose only raw `str` and function-valued `Var` inputs.</comment>

<file context>
@@ -136,11 +136,28 @@ def create(cls, *children, **props):
+                    "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."
+                )
</file context>
Fix with cubic

"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)',
Expand Down Expand Up @@ -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"]

Expand Down
2 changes: 1 addition & 1 deletion pyi_hashes.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
45 changes: 45 additions & 0 deletions tests/units/components/recharts/test_cartesian.py
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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"