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
146 changes: 146 additions & 0 deletions docs/library/graphing/charts/sankeychart.md
Original file line number Diff line number Diff line change
@@ -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)
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@
"ScatterChart",
"funnel_chart",
"FunnelChart",
"sankey_chart",
"SankeyChart",
"treemap",
"Treemap",
],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,14 @@
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
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

Expand Down Expand Up @@ -516,6 +517,88 @@ class FunnelChart(ChartBase):
]


class SankeyNode(TypedDict):
"""A node in a Sankey chart."""

name: str
type: NotRequired[str]
fill: NotRequired[str | Color]
stroke: NotRequired[str | Color]
strokeWidth: NotRequired[int | float]
strokeOpacity: NotRequired[int | float]


class SankeyLink(TypedDict):
"""A weighted link between two Sankey chart nodes."""

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):
"""The source data for a Sankey chart."""

nodes: Sequence[SankeyNode]
links: Sequence[SankeyLink]


Comment thread
greptile-apps[bot] marked this conversation as resolved.
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."
)
Comment on lines +562 to +564

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 Badge Accept State vars for Sankey data

With data typed as the exact SankeyData TypedDict, data=SankeyState.data is rejected before rendering when the State var is inferred or annotated as a normal mapping, which is exactly what the new stateful docs example does with its unannotated dict. Component._post_init checks a Var's _var_type against this prop hint, and a Mapping[...]/dict[...] State var is not a subclass of this TypedDict, so dynamic Sankey charts fail even though direct dict literals work. Use a less restrictive mapping-based prop type, or make the exact annotation part of the public API and docs.

Useful? React with 👍 / 👎.


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."""

Expand Down Expand Up @@ -598,4 +681,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
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ class ResponsiveContainer(Recharts, MemoizationLeaf):
"RadialBarChart",
"ResponsiveContainer",
"ScatterChart",
"SankeyChart",
"Treemap",
"ComposedChart",
"FunnelChart",
Expand Down
4 changes: 2 additions & 2 deletions pyi_hashes.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": "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",
Expand Down
8 changes: 8 additions & 0 deletions tests/units/components/graphing/test_recharts.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
PieChart,
RadarChart,
RadialBarChart,
SankeyChart,
ScatterChart,
)
from reflex_components_recharts.general import ResponsiveContainer
Expand Down Expand Up @@ -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"