Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
e94c110
Wrap Recharts Sankey chart
Jul 6, 2026
574f851
Tighten Sankey data typing
Jul 6, 2026
00fa111
Tighten Sankey chart wrapper props
Jul 10, 2026
cc9bd95
Add Sankey chart changelog fragment
Jul 10, 2026
47f21ad
Merge remote-tracking branch 'origin/main' into fix/wrap-recharts-san…
masenf Jul 29, 2026
900ab4d
Enable proper custom sankey node and link renderers
masenf Jul 30, 2026
6c9066d
reflex-components-recharts depends on reflex-base `use_hook_var` and …
masenf Jul 30, 2026
cb445be
Add changelog, tests, and docs for reflex_base.vars.special
claude Jul 30, 2026
ad5de4f
Fix Sankey payload typing and package build
Aug 1, 2026
7a9c158
Address Sankey renderer review feedback
Aug 1, 2026
6a09841
Address Sankey docs and namespace feedback
Aug 1, 2026
aa25c05
Address final Sankey review nits
Aug 4, 2026
bd6054f
Merge remote-tracking branch 'upstream/main' into fix/wrap-recharts-s…
Aug 4, 2026
a217435
Merge branch 'main' into fix/wrap-recharts-sankey-6558
harsh21234i Aug 7, 2026
d9af11f
Merge branch 'main' into fix/wrap-recharts-sankey-6558
harsh21234i Aug 9, 2026
2f98d91
Merge branch 'main' into fix/wrap-recharts-sankey-6558
harsh21234i Aug 13, 2026
43e4f1a
Fix Sankey docs renderer type
Aug 13, 2026
ad58f95
Merge branch 'main' into fix/wrap-recharts-sankey-6558
harsh21234i Aug 21, 2026
46b2b5e
Merge branch 'main' into fix/wrap-recharts-sankey-6558
harsh21234i Aug 25, 2026
3e662db
fix docs Sankey SVG coordinate types
harsh21234i Sep 9, 2026
07c41fd
fix Sankey node coordinate types
harsh21234i Sep 9, 2026
ff15e6a
refactor hook identifiers into constants
harsh21234i Sep 9, 2026
d80d777
update generated pyi hash
harsh21234i Sep 9, 2026
9c1bc20
fix Sankey typing and hook documentation
harsh21234i Sep 10, 2026
a29b860
merge current main into Sankey PR
harsh21234i Sep 10, 2026
561e403
Update generated Recharts stub hashes
harsh21234i Sep 10, 2026
c1939a2
Drop the hook vars from the Sankey wrapper and export its TypedDicts
FarhanAliRaza Sep 10, 2026
5cfa5c2
Restore the hook vars and make rx.el.svg a single render scope
FarhanAliRaza Sep 10, 2026
e4f46c3
Cover the package-level Sankey exports and document hook var scoping
FarhanAliRaza Sep 10, 2026
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
36 changes: 36 additions & 0 deletions docs/api-reference/var_system.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,3 +78,39 @@ def factorial(value: NumberVar):

Use `js_expression` to pass explicit JavaScript expressions; in the `multiply_array_values` example, we pass in a JavaScript expression that calculates the product of all elements in an array called `a` by using the reduce method to multiply each element with the accumulated result, starting from an initial value of 1.
Later, we leverage `rx.cond` in the' factorial' function, we instantiate an array using the `range` function, and pass this array to `multiply_array_values`.

## Hook Vars

Some values only exist on the frontend and are exposed through React hooks.
`rx.vars.use_hook_var()` binds the return value of a no-argument hook to a unique variable name and returns it as a `Var`.
The hook call and the import of the hook are automatically included in any component that uses the var, so the value reflects the context of the component it is rendered in.

```py
chart_width = rx.vars.use_hook_var(
library="recharts@3.8.1",
hook="useChartWidth",
_var_type=int | None,
)
```

A component using `chart_width` will import `useChartWidth` from `recharts` and render `const <unique_name> = useChartWidth();` in its body, so `chart_width` can be used like any other `Var[int | None]`.

A hook var is evaluated once per compiled component, so every element that reads it must render inside the same one. An `rx.el.svg` root, an `@rx.memo` body, and a custom renderer body each compile into a single component and satisfy this.

For the common case of React's built-in [`useId`](https://react.dev/reference/react/useId), `rx.vars.use_id()` returns a `Var[str]` containing a stable unique id for the rendered component.
This is useful for linking SVG elements to `defs` such as gradients or filters:

```py
def gradient_rect() -> rx.Component:
gradient_id = rx.vars.use_id()
return rx.el.svg(
rx.el.svg.linear_gradient(
rx.el.svg.stop(offset="0%", stop_color="gold"),
rx.el.svg.stop(offset="100%", stop_color="tomato"),
id=gradient_id,
),
rx.el.svg.rect(fill=f"url(#{gradient_id})", width=64, height=64),
width=64,
height=64,
)
```
221 changes: 221 additions & 0 deletions docs/library/graphing/charts/sankeychart.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
---
components:
- rx.recharts.SankeyChart
title: Sankey Chart
meta_description: "Create Sankey charts in Python with Reflex. Build interactive Recharts Sankey diagrams to visualize weighted flows between stages, categories, or systems."
---

# Sankey Chart

```python exec
import random
from typing import Any

import reflex as rx
```

Sankey charts in Reflex are built on [Recharts](https://recharts.org/), a React charting library, and created in pure Python. A Sankey chart visualizes weighted flows between nodes, making it useful for showing movement through stages, resource allocation, user journeys, and other source-to-target relationships.

## Simple Example

An `rx.recharts.sankey_chart()` takes a `data` dictionary with `nodes` and `links`. Links refer to nodes by zero-based index.

```python demo graphing
sankey_data = {
"nodes": [
{"name": "Website"},
{"name": "Landing Page"},
{"name": "Product Page"},
{"name": "Checkout"},
{"name": "Purchase"},
],
"links": [
{"source": 0, "target": 1, "value": 1200},
{"source": 1, "target": 2, "value": 900},
{"source": 2, "target": 3, "value": 420},
{"source": 3, "target": 4, "value": 260},
],
}


def sankey_simple():
return rx.recharts.sankey_chart(
rx.recharts.graphing_tooltip(),
data=sankey_data,
node_padding=24,
node_width=12,
link_curvature=0.55,
width="100%",
height=320,
)
```

## Stateful Example

Chart data can be tied to a State var. This example randomizes the flow values when the button is clicked.

```python demo exec
class SankeyState(rx.State):
Comment thread
harsh21234i marked this conversation as resolved.
data: dict[str, Any] = {
"nodes": [
{"name": "Marketing"},
{"name": "Trial"},
{"name": "Sales"},
{"name": "Support"},
{"name": "Retained"},
],
"links": [
{"source": 0, "target": 1, "value": 600},
{"source": 1, "target": 2, "value": 320},
{"source": 2, "target": 4, "value": 210},
{"source": 1, "target": 3, "value": 180},
{"source": 3, "target": 4, "value": 130},
],
}

@rx.event
def randomize_flows(self):
for link in self.data["links"]:
link["value"] = random.randint(80, 700)


def sankey_stateful():
return rx.vstack(
rx.recharts.sankey_chart(
rx.recharts.graphing_tooltip(),
data=SankeyState.data,
node={
"fill": rx.color("accent", 7),
"stroke": rx.color("accent", 10),
"strokeWidth": 2,
},
link={
"stroke": rx.color("gray", 7),
"strokeOpacity": 0.35,
},
node_padding=18,
node_width=14,
width="100%",
height=320,
),
rx.button("Randomize flows", on_click=SankeyState.randomize_flows),
width="100%",
)
```


## Full Node / Link Customization

For complete control over node and link rendering, pass a
`@rx.recharts.sankey_chart.node` or `@rx.recharts.sankey_chart.link` decorated
function that returns an svg-based component. The function receives a
`Var[SankeyNodeProps]` or `Var[SankeyLinkProps]` object with the node or link
data `payload`, as well as the object's position and dimensions. You can use these
properties to construct a custom node or link.

Because the component renders inside the SVG element of the chart, you can only
use `rx.el.svg` components to construct the custom node or link.

The example below also uses `rx.recharts.use_chart_width()` to read the
rendered chart width and `rx.vars.use_id()` to generate a unique id that links
each link's gradient definition to the path that references it.

```python demo graphing
styled_sankey_data = {
"nodes": [
{"name": "Sources", "type": "source", "fill": rx.color("blue", 8)},
{"name": "Direct", "type": "channel", "fill": rx.color("green", 8)},
{"name": "Search", "type": "channel", "fill": rx.color("grass", 8)},
{"name": "Paid", "type": "channel", "fill": rx.color("amber", 8)},
{"name": "Revenue", "type": "outcome", "fill": rx.color("purple", 8)},
],
"links": [
{"source": 0, "target": 1, "value": 350},
{"source": 0, "target": 2, "value": 500},
{"source": 0, "target": 3, "value": 220},
{"source": 1, "target": 4, "value": 190},
{"source": 2, "target": 4, "value": 260},
{"source": 3, "target": 4, "value": 150},
],
}


def sankey_custom_render():
@rx.recharts.sankey_chart.node
def custom_node(
node: rx.Var[rx.recharts.SankeyNodeProps],
) -> rx.Component:
# Determine if the node is at the right edge of the chart to adjust the label position accordingly.
is_out = node.x + node.width + 6 > rx.recharts.use_chart_width()
return rx.fragment(
rx.el.svg.text(
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
node.payload.name,
x=rx.cond(is_out, node.x - 6, node.x + node.width + 6).to(int),
y=(node.y + node.height / 2).to(int),
text_anchor=rx.cond(is_out, "end", "start"),
fill=rx.color("gray", 12),
font_size=10,
),
rx.el.svg.rect(
x=node.x.to(int),
y=node.y.to(int),
width=node.width.to(int),
height=node.height.to(int),
# Accessing custom keys in the payload needs a `dict` cast.
fill=node.payload.to(dict)["fill"],
stroke=rx.color("gray", 12),
Comment thread
harsh21234i marked this conversation as resolved.
stroke_width=1,
),
)

@rx.recharts.sankey_chart.link
def custom_link(
link: rx.Var[rx.recharts.SankeyLinkProps],
) -> rx.Component:
link_id = rx.vars.use_id()
source = link.payload.source.to(dict)
target = link.payload.target.to(dict)
return rx.fragment(
rx.el.svg.linear_gradient(
rx.el.svg.stop(offset="0%", stop_color=source["fill"]),
rx.el.svg.stop(offset="100%", stop_color=target["fill"]),
id=link_id,
),
rx.el.svg.path(
d=(
f"M{link.sourceX},{link.sourceY} "
f"C{link.sourceControlX},{link.sourceY} "
f"{link.targetControlX},{link.targetY} "
f"{link.targetX},{link.targetY}"
),
fill="none",
stroke=f"url(#{link_id})",
stroke_opacity=0.35,
stroke_width=link.linkWidth,
),
rx.el.svg.text(
link.payload.value,
x=((link.sourceX + link.targetX) / 2).to(int),
y=((link.sourceY + link.targetY) / 2).to(int),
text_anchor="middle",
fill=rx.color("gray", 12),
font_size=10,
),
)

return rx.recharts.sankey_chart(
data=styled_sankey_data,
node=custom_node,
link=custom_link,
width="100%",
height=340,
)
```

## Related Charts

Explore more chart types you can build with Reflex and Recharts in pure Python:

- [Treemap](/docs/library/graphing/charts/treemap)
- [Funnel Chart](/docs/library/graphing/charts/funnelchart)
- [Pie Chart](/docs/library/graphing/charts/piechart)
19 changes: 19 additions & 0 deletions docs/wrapping-react/custom-code-and-hooks.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,3 +114,22 @@ export function Div_7178f430b7b371af8a12d8265d65ab9b() {
```md alert info
# You can mix custom code and hooks in the same component. Hooks can access a variable defined in the custom code, but custom code cannot access a variable defined in a hook.
```

## Using a Hook's Return Value

`add_hooks` inserts hook statements into the component, but the values they define are not directly accessible from Python. When you need the return value of a no-argument hook, use `rx.vars.use_hook_var()`, which binds the hook call to a unique variable name and returns it as a `Var`. The hook statement and its import are automatically included in any component where the var is used, so it composes with regular props and var operations.

```python
import reflex as rx


def use_chart_width() -> rx.Var[int | None]:
"""Get the width of the enclosing recharts chart as a var."""
return rx.vars.use_hook_var(
library="recharts@3.8.1", hook="useChartWidth", _var_type=int | None
)
```

For React's built-in [`useId`](https://react.dev/reference/react/useId), `rx.vars.use_id()` returns a `Var[str]` with a stable unique id for the component being rendered, e.g. for linking SVG elements to gradient or filter definitions.
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.

A hook var is evaluated once per compiled component, so every element that reads it must render inside the same one. An `rx.el.svg` root, an `@rx.memo` body, and a custom renderer body each compile into a single component and satisfy this.
1 change: 1 addition & 0 deletions packages/reflex-base/news/6708.feature.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add `rx.vars.use_hook_var()` to create a `Var` bound to the value of a no-argument React hook imported from a given library, and `rx.vars.use_id()` to get React's stable `useId` value for the rendered component.
3 changes: 3 additions & 0 deletions packages/reflex-base/src/reflex_base/vars/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
LiteralStringVar,
StringVar,
)
from .special import use_hook_var, use_id

__all__ = [
"EMPTY_VAR_INT",
Expand Down Expand Up @@ -66,6 +67,8 @@
"number",
"object",
"sequence",
"use_hook_var",
"use_id",
"var_operation",
"var_operation_return",
]
64 changes: 64 additions & 0 deletions packages/reflex-base/src/reflex_base/vars/special.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
"""Special Vars for rendering values from the environment."""

from types import UnionType
from typing import Any, TypeVar, cast, overload

from typing_extensions import TypeForm

from reflex_base.utils.imports import ImportVar
from reflex_base.utils.types import GenericType
from reflex_base.vars.base import Var, VarData, get_unique_variable_name

HOOK_VAR_TYPE = TypeVar("HOOK_VAR_TYPE")
_REACT_LIBRARY = "react"
_USE_ID_HOOK = "useId"


@overload
def use_hook_var(library: str, hook: str) -> Var[Any]: ...


@overload
def use_hook_var(
library: str, hook: str, _var_type: TypeForm[HOOK_VAR_TYPE]
) -> Var[HOOK_VAR_TYPE]: ...


@overload
def use_hook_var(library: str, hook: str, _var_type: UnionType) -> Var[Any]: ...


def use_hook_var(library: str, hook: str, _var_type: Any = Any) -> Var:
"""Get a Var representing a React hook's value.

The hook is called once in each compiled component that reads the var, so
every element sharing one value must render inside the same component, such
as an ``rx.el.svg`` root, an ``@rx.memo`` body, or a custom renderer body.

Args:
library: The library to import the hook from.
hook: The name of the hook.
_var_type: The type of the Var.

Returns:
A Var representing the React hook.
"""
var_name = get_unique_variable_name()
hook_alias = f"{hook}_{var_name}"
return Var(
var_name,
_var_type=cast(GenericType, _var_type),
_var_data=VarData(
imports={library: ImportVar(tag=hook, alias=hook_alias)},
hooks=(f"const {var_name} = {hook_alias}();",),
),
).guess_type()


def use_id() -> Var[str]:
"""Get the stable React useId hook value for a component.

Returns:
A Var representing the useId hook value.
"""
return use_hook_var(library=_REACT_LIBRARY, hook=_USE_ID_HOOK, _var_type=str)
1 change: 1 addition & 0 deletions packages/reflex-components-core/news/6708.bugfix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
`rx.el.svg` and its children now render as one memoized component, so `defs` such as gradients and the elements that reference them by id always share one render scope.
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

from reflex_base.components.component import Component, ComponentNamespace, field
from reflex_base.constants.colors import Color
from reflex_base.constants.compiler import MemoizationMode
from reflex_base.vars.base import Var

from reflex_components_core.el.elements.inline import ReferrerPolicy
Expand Down Expand Up @@ -274,9 +275,15 @@ class Source(VoidBaseHTML):


class Svg(BaseHTML):
"""Display the svg element."""
"""Display the svg element.

The svg root and its descendants render as a single memoized component,
so ``defs`` and the elements that reference them by id share one render
scope and hook vars such as ``rx.vars.use_id()`` resolve to one value.
"""

tag = "svg"
_memoization_mode = MemoizationMode(recursive=False)
width: Var[str | int] = field(doc="The width of the svg.")
height: Var[str | int] = field(doc="The height of the svg.")
xmlns: Var[str] = field(doc="The XML namespace declaration.")
Expand Down
1 change: 1 addition & 0 deletions packages/reflex-components-recharts/news/6708.feature.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Added a Recharts Sankey chart wrapper (`rx.recharts.sankey_chart`) with support for custom node and link renderers, and `rx.recharts.use_chart_width()` for reading the rendered chart width as a `Var`.
Loading
Loading