From e94c1102db01639a8317215e925adc919bb29da2 Mon Sep 17 00:00:00 2001 From: Harsh Thakare Date: Mon, 6 Jul 2026 21:20:25 +0530 Subject: [PATCH 1/2] Wrap Recharts Sankey chart --- docs/library/graphing/charts/sankeychart.md | 146 ++++++++++++++++++ .../reflex_components_recharts/__init__.py | 2 + .../src/reflex_components_recharts/charts.py | 80 +++++++++- .../src/reflex_components_recharts/general.py | 1 + pyi_hashes.json | 4 +- .../components/graphing/test_recharts.py | 8 + 6 files changed, 238 insertions(+), 3 deletions(-) create mode 100644 docs/library/graphing/charts/sankeychart.md diff --git a/docs/library/graphing/charts/sankeychart.md b/docs/library/graphing/charts/sankeychart.md new file mode 100644 index 00000000000..bea8e864dd6 --- /dev/null +++ b/docs/library/graphing/charts/sankeychart.md @@ -0,0 +1,146 @@ +--- +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 + +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): + data = { + "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_padding=18, + node_width=14, + width="100%", + height=320, + ), + rx.button("Randomize flows", on_click=SankeyState.randomize_flows), + width="100%", + ) +``` + +## Custom Node Types And Styles + +Use fields on each node to describe node types and per-node styling. Pass `node` or `link` dictionaries to control shared Sankey styling. + +```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_styles(): + return rx.recharts.sankey_chart( + rx.recharts.graphing_tooltip(), + data=styled_sankey_data, + node={ + "stroke": rx.color("gray", 12), + "strokeWidth": 1, + }, + link={ + "stroke": rx.color("gray", 8), + "strokeOpacity": 0.35, + }, + node_padding=22, + node_width=16, + link_curvature=0.45, + 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) diff --git a/packages/reflex-components-recharts/src/reflex_components_recharts/__init__.py b/packages/reflex-components-recharts/src/reflex_components_recharts/__init__.py index bb327bbb54d..cb5bdff211b 100644 --- a/packages/reflex-components-recharts/src/reflex_components_recharts/__init__.py +++ b/packages/reflex-components-recharts/src/reflex_components_recharts/__init__.py @@ -56,6 +56,8 @@ "ScatterChart", "funnel_chart", "FunnelChart", + "sankey_chart", + "SankeyChart", "treemap", "Treemap", ], diff --git a/packages/reflex-components-recharts/src/reflex_components_recharts/charts.py b/packages/reflex-components-recharts/src/reflex_components_recharts/charts.py index 6bfedd0a451..f0879ebff77 100644 --- a/packages/reflex-components-recharts/src/reflex_components_recharts/charts.py +++ b/packages/reflex-components-recharts/src/reflex_components_recharts/charts.py @@ -3,7 +3,7 @@ from __future__ import annotations from collections.abc import Sequence -from typing import Any, ClassVar +from typing import Any, ClassVar, TypedDict from reflex_base.components.component import Component, field from reflex_base.constants import EventTriggers @@ -516,6 +516,83 @@ class FunnelChart(ChartBase): ] +class SankeyNode(TypedDict, total=False): + """A node in a Sankey chart.""" + + name: str + type: str + fill: str | Color + stroke: str | Color + strokeWidth: int | float + strokeOpacity: int | float + + +class SankeyLink(TypedDict): + """A weighted link between two Sankey chart nodes.""" + + source: int + target: int + value: int | float + + +class SankeyData(TypedDict): + """The source data for a Sankey chart.""" + + nodes: Sequence[SankeyNode] + links: Sequence[SankeyLink] + + +class SankeyChart(ChartBase): + """A Sankey chart component in Recharts.""" + + tag = "Sankey" + + alias = "RechartsSankeyChart" + + name_key: Var[str] = field(doc='The key of each node name. Default: "name"') + + data_key: Var[str | int] = field(doc='The key of each link value. Default: "value"') + + data: Var[SankeyData] = field( + doc="The source data, including nodes and the weighted links between them." + ) + + margin: Var[dict[str, Any]] = field( + doc='The sizes of whitespace around the chart. Default: {"top": 5, "right": 5, "bottom": 5, "left": 5}' + ) + + node: Var[Any] = field( + doc="The configuration object or custom renderer used to draw nodes." + ) + + link: Var[Any] = field( + doc="The configuration object or custom renderer used to draw links." + ) + + sort: Var[bool] = field( + doc="Whether to sort nodes on the y-axis or display them in data order." + ) + + node_padding: Var[int] = field(doc="The padding between nodes.") + + node_width: Var[int] = field(doc="The width of each node.") + + link_width: Var[int] = field(doc="The width of each link.") + + link_curvature: Var[float] = field(doc="The curvature of each link.") + + iterations: Var[int] = field( + doc="The number of layout iterations used to position nodes and links." + ) + + # Valid children components + _valid_children: ClassVar[list[str]] = [ + "Legend", + "GraphingTooltip", + "Defs", + ] + + class Treemap(RechartsCharts): """A Treemap chart component in Recharts.""" @@ -598,4 +675,5 @@ def create(cls, *children, **props) -> Component: radial_bar_chart = RadialBarChart.create scatter_chart = ScatterChart.create funnel_chart = FunnelChart.create +sankey_chart = SankeyChart.create treemap = Treemap.create diff --git a/packages/reflex-components-recharts/src/reflex_components_recharts/general.py b/packages/reflex-components-recharts/src/reflex_components_recharts/general.py index d3dcaa97926..14611aff8aa 100644 --- a/packages/reflex-components-recharts/src/reflex_components_recharts/general.py +++ b/packages/reflex-components-recharts/src/reflex_components_recharts/general.py @@ -66,6 +66,7 @@ class ResponsiveContainer(Recharts, MemoizationLeaf): "RadialBarChart", "ResponsiveContainer", "ScatterChart", + "SankeyChart", "Treemap", "ComposedChart", "FunnelChart", diff --git a/pyi_hashes.json b/pyi_hashes.json index f5b97d72d2b..7a6097c07ad 100644 --- a/pyi_hashes.json +++ b/pyi_hashes.json @@ -111,9 +111,9 @@ "packages/reflex-components-react-player/src/reflex_components_react_player/audio.pyi": "39e4144cef066bbff8c27b36a205a54b", "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/__init__.pyi": "c2ab419b100855925a6511b53b668f0f", "packages/reflex-components-recharts/src/reflex_components_recharts/cartesian.pyi": "3485aaabbfd3ca89908ae19425c7297e", - "packages/reflex-components-recharts/src/reflex_components_recharts/charts.pyi": "280a7cd51298ee676f3d076104133f44", + "packages/reflex-components-recharts/src/reflex_components_recharts/charts.pyi": "2a415854f1507ffcfb166884c318d0c4", "packages/reflex-components-recharts/src/reflex_components_recharts/general.pyi": "f5e3491c4e1f69ba89085682888888a9", "packages/reflex-components-recharts/src/reflex_components_recharts/polar.pyi": "99ebcfc07868061bdc3c2010d85a153f", "packages/reflex-components-recharts/src/reflex_components_recharts/recharts.pyi": "4f6c26f8c76543cc41e2b9dc400ece8a", diff --git a/tests/units/components/graphing/test_recharts.py b/tests/units/components/graphing/test_recharts.py index 3e4268eb891..710e2c23712 100644 --- a/tests/units/components/graphing/test_recharts.py +++ b/tests/units/components/graphing/test_recharts.py @@ -5,6 +5,7 @@ PieChart, RadarChart, RadialBarChart, + SankeyChart, ScatterChart, ) from reflex_components_recharts.general import ResponsiveContainer @@ -50,3 +51,10 @@ def test_scatter_chart(): sc = ScatterChart.create() assert isinstance(sc, ResponsiveContainer) assert isinstance(sc.children[0], ScatterChart) + + +def test_sankey_chart(): + sc = SankeyChart.create() + assert isinstance(sc, ResponsiveContainer) + assert isinstance(sc.children[0], SankeyChart) + assert sc.children[0].render()["name"] == "RechartsSankeyChart" From 574f8510d78fe041f9775d7bb0e7924316e8c580 Mon Sep 17 00:00:00 2001 From: Harsh Thakare Date: Mon, 6 Jul 2026 22:05:21 +0530 Subject: [PATCH 2/2] Tighten Sankey data typing --- .../src/reflex_components_recharts/charts.py | 18 ++++++++++++------ pyi_hashes.json | 2 +- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/packages/reflex-components-recharts/src/reflex_components_recharts/charts.py b/packages/reflex-components-recharts/src/reflex_components_recharts/charts.py index f0879ebff77..1a9bbc404ea 100644 --- a/packages/reflex-components-recharts/src/reflex_components_recharts/charts.py +++ b/packages/reflex-components-recharts/src/reflex_components_recharts/charts.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 Var +from typing_extensions import NotRequired from reflex_components_recharts.general import ResponsiveContainer @@ -516,15 +517,15 @@ class FunnelChart(ChartBase): ] -class SankeyNode(TypedDict, total=False): +class SankeyNode(TypedDict): """A node in a Sankey chart.""" name: str - type: str - fill: str | Color - stroke: str | Color - strokeWidth: int | float - strokeOpacity: int | float + type: NotRequired[str] + fill: NotRequired[str | Color] + stroke: NotRequired[str | Color] + strokeWidth: NotRequired[int | float] + strokeOpacity: NotRequired[int | float] class SankeyLink(TypedDict): @@ -533,6 +534,11 @@ class SankeyLink(TypedDict): source: int target: int value: int | float + fill: NotRequired[str | Color] + fillOpacity: NotRequired[int | float] + stroke: NotRequired[str | Color] + strokeWidth: NotRequired[int | float] + strokeOpacity: NotRequired[int | float] class SankeyData(TypedDict): diff --git a/pyi_hashes.json b/pyi_hashes.json index 7a6097c07ad..446ed12fb5d 100644 --- a/pyi_hashes.json +++ b/pyi_hashes.json @@ -113,7 +113,7 @@ "packages/reflex-components-react-player/src/reflex_components_react_player/video.pyi": "2682dc6e825d25307d8390d01e2bd653", "packages/reflex-components-recharts/src/reflex_components_recharts/__init__.pyi": "c2ab419b100855925a6511b53b668f0f", "packages/reflex-components-recharts/src/reflex_components_recharts/cartesian.pyi": "3485aaabbfd3ca89908ae19425c7297e", - "packages/reflex-components-recharts/src/reflex_components_recharts/charts.pyi": "2a415854f1507ffcfb166884c318d0c4", + "packages/reflex-components-recharts/src/reflex_components_recharts/charts.pyi": "362e3055c2ebff0881860f87a459940e", "packages/reflex-components-recharts/src/reflex_components_recharts/general.pyi": "f5e3491c4e1f69ba89085682888888a9", "packages/reflex-components-recharts/src/reflex_components_recharts/polar.pyi": "99ebcfc07868061bdc3c2010d85a153f", "packages/reflex-components-recharts/src/reflex_components_recharts/recharts.pyi": "4f6c26f8c76543cc41e2b9dc400ece8a",