R

ReflexKR

Reflex Docs

The Component Workshop: Reflex's 100 Library Components

·

Course overview

Layout

·

Box

·

Flex

·

Stack

·

Grid

·

Container

·

Section

·

Center

·

Spacer

·

Separator

·

Card

·

Inset

·

Aspect Ratio

·

Fragment

Typography

·

Text

·

Heading

·

Link

·

Code

·

Markdown

·

Blockquote

·

Quote

·

Strong

·

Em

·

Kbd

Forms

·

Button

·

Input

·

Text Area

·

Checkbox

·

Switch

·

Radio Group

·

Select

·

Slider

·

Form

·

Upload

Data display

·

Badge

·

Avatar

·

Icon

·

Callout

·

List

·

Data List

·

Code Block

·

Progress

·

Spinner

·

Scroll Area

·

Moment

Disclosure

·

Tabs

·

Accordion

·

Segmented Control

Overlay

·

Tooltip

·

Popover

·

Hover Card

·

Dialog

·

Alert Dialog

·

Drawer

·

Dropdown Menu

·

Context Menu

·

Toast

Dynamic rendering

·

Cond

·

Match

·

Foreach

·

Auto Scroll

Media

·

Image

·

Audio

·

Video

Tables and data grids

·

Table

·

Data Table

·

Data Editor

Charts

·

Area Chart

·

Bar Chart

·

Line Chart

·

Composed Chart

·

Pie Chart

·

Radar Chart

·

Radial Bar Chart

·

Scatter Chart

·

Funnel Chart

·

Treemap

·

Sankey Chart

·

Error Bar

Chart parts

·

Axis

·

Cartesian Grid

·

Chart Tooltip

·

Legend

·

Label

·

Reference

·

Brush

Other chart libraries

·

Plotly

·

Pyplot

HTML elements

·

HTML

·

HTML Text

·

HTML Layout

·

HTML Forms

·

HTML Media

·

HTML Tables

·

SVG

Other tools

·

Theme

·

Skeleton

·

Clipboard

·

Html Embed

·

Script

·

Memo

The Component Workshop: Reflex's 100 Library Components
Charts

Sankey Chart

rx.recharts.sankey_chartrx.recharts.sankey_chart.noderx.recharts.sankey_chart.link

Official docs

Workshop order

Please show where a month's income comes from and how much of it flows to rent, food and savings, using the thickness of the flows.

rx.recharts.sankey_chart is a Sankey diagram: it shows nodes and the amounts flowing between them as bands whose thickness matches the amount. Use it to look at "flows", such as which pages visitors pass through on the way to a purchase, or where money comes from and where it goes.

data is not a list but a dict shaped like {"nodes": [...], "links": [...]}. A link's source and target are positions (from 0) in the nodes list, and value is the size of that flow. The tooltip goes in as a child.

sankeychart_data = {
    "nodes": [
        {"name": "Website"},
        {"name": "Landing"},
        {"name": "Product"},
        {"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},
    ],
}


rx.recharts.sankey_chart(
    rx.recharts.graphing_tooltip(),
    data=sankeychart_data,
    node_padding=24,
    node_width=12,
    link_curvature=0.55,
    width="100%",
    height=300,
)

Try the props

A dict for node or link applies the same SVG styles to every node or band. Write the dict keys exactly as SVG attribute names (in camelCase, like strokeOpacity). link_curvature is how much the bands bend (0 is straight).

Try the props

node_color

link_opacity

0.35

link_curvature

0.5

sort

rx.recharts.sankey_chart(
    rx.recharts.graphing_tooltip(),
    data={
        "nodes": [{"name": "Ads"}, {"name": "Search"}, {"name": "Signup"}, {"name": "Leave"}, {"name": "Paid"}],
        "links": [
            {"source": 0, "target": 2, "value": 300},
            {"source": 0, "target": 3, "value": 500},
            {"source": 1, "target": 2, "value": 600},
            {"source": 1, "target": 3, "value": 200},
            {"source": 2, "target": 4, "value": 350},
        ],
    },
    node={"fill": rx.color("indigo", 9), "stroke": "none"},
    link={"stroke": rx.color("gray", 9), "strokeOpacity": 0.35},
    link_curvature=0.5,
    sort=True,
    node_padding=20,
    node_width=12,
    width="100%",
    height=300,
)
With sort off, nodes are placed in the order they appear in the data; with it on, their vertical order is chosen so the bands tangle less.
PropValuesDefaultUse
data{"nodes": [...], "links": [...]}noneNodes and flows. Links connect nodes by position
name_keystring"name"Key of the node name
data_keystring"value"Key of the flow amount
nodedict or a @sankey_chart.node functionNode style, or a function that draws nodes yourself
linkdict or a @sankey_chart.link functionBand style, or a function that draws bands yourself
node_widthnumber10Thickness of the node bars
node_paddingnumber10Gap between nodes in the same column
link_curvature0–10.5How much the bands bend
sortTrue / FalseTrueWhether to sort nodes vertically
iterationsnumber32How many times the layout calculation repeats
margin, width, heightdict / number or percentage5 each / "100%"Margin and size. Widen margin if names on the right get cut off
Eventson_click on_mouse_enter on_mouse_leave on_mouse_moveMouse events on the chart area

Flows that change with State

data is connected to a dict in State. Press the button and the links get new values, so the band thickness and node heights are recalculated. Here node and link get dicts for color, border and opacity.

import random
from typing import Any


class SankeyChartLiveState(rx.State):
    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 shuffle(self):
        self.data = {
            "nodes": self.data["nodes"],
            "links": [{**link, "value": random.randint(80, 700)} for link in self.data["links"]],
        }


rx.vstack(
    rx.recharts.sankey_chart(
        rx.recharts.graphing_tooltip(),
        data=SankeyChartLiveState.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=300,
    ),
    rx.button("Randomize flows", on_click=SankeyChartLiveState.shuffle),
    width="100%",
)

Drawing nodes and bands yourself

When a dict isn't enough, write functions that draw the nodes and bands. A function decorated with @rx.recharts.sankey_chart.node receives one node's information as a Var (SankeyNodeProps: position x and y, size width and height, and the original data payload) and returns an SVG component. @rx.recharts.sankey_chart.link receives one band's information (SankeyLinkProps: start and end coordinates, thickness linkWidth, and payload).

  • rx.recharts.use_chart_width() is the chart's actual width. It is used to put the names of the rightmost nodes on their left.
  • Custom keys you put in the data ("fill") are read by converting to a dict, as in payload.to(dict)["fill"].
  • rx.vars.use_id() creates a unique gradient id for each band. Each band blends from the source node's color to the target node's color.

Define the two functions once at module level before using them in the UI.

sankeychart_styled = {
    "nodes": [
        {"name": "Sources", "fill": rx.color("blue", 8)},
        {"name": "Direct", "fill": rx.color("green", 8)},
        {"name": "Search", "fill": rx.color("grass", 8)},
        {"name": "Paid", "fill": rx.color("amber", 8)},
        {"name": "Revenue", "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},
    ],
}


@rx.recharts.sankey_chart.node
def sankeychart_node(node: rx.Var[rx.recharts.SankeyNodeProps]) -> rx.Component:
    # put the name on the left for the rightmost nodes
    is_out = node.x + node.width + 6 > rx.recharts.use_chart_width()
    return rx.fragment(
        rx.el.svg.text(
            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),
            # read the payload's custom key (fill) after converting it to a dict
            fill=node.payload.to(dict)["fill"],
            stroke=rx.color("gray", 12),
            stroke_width=1,
        ),
    )


@rx.recharts.sankey_chart.link
def sankeychart_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,
        ),
    )


rx.recharts.sankey_chart(
    data=sankeychart_styled,
    node=sankeychart_node,
    link=sankeychart_link,
    width="100%",
    height=320,
)

Filling the order

Two income sources merge into a "Budget" node and then split three ways. Nodes are painted in a strong color and bands in a lighter shade of the same color, so the flows seem to pour out of the nodes, and a wider right margin keeps the names in the last column from being cut off.

rx.recharts.sankey_chart(
    rx.recharts.graphing_tooltip(),
    data={
        "nodes": [
            {"name": "Salary"},
            {"name": "Side job"},
            {"name": "Budget"},
            {"name": "Rent"},
            {"name": "Food"},
            {"name": "Savings"},
        ],
        "links": [
            {"source": 0, "target": 2, "value": 3000},
            {"source": 1, "target": 2, "value": 600},
            {"source": 2, "target": 3, "value": 1400},
            {"source": 2, "target": 4, "value": 900},
            {"source": 2, "target": 5, "value": 1300},
        ],
    },
    node={"fill": rx.color("accent", 9), "stroke": "none"},
    link={"stroke": rx.color("accent", 6), "strokeOpacity": 0.5},
    node_padding=28,
    node_width=10,
    margin={"top": 10, "right": 80, "bottom": 10, "left": 10},
    width="100%",
    height=300,
)

Workshop notes

  • data is {"nodes": [...], "links": [...]}. A link's source and target are node positions.

  • A dict for node or link styles them all (SVG attribute names in camelCase).

  • Tune the layout with node_width, node_padding, link_curvature and sort.

  • For more freedom, return SVG from @rx.recharts.sankey_chart.node / .link functions.

  • Connect data to a dict in State and the flows are recalculated when it changes.

← Previous

Charts · Treemap

Next →

Charts · Error Bar

Get news about new lessons

We'll email you when this course gets new lessons or a follow-up course opens.

We collect: your email address · Purpose: news about new lessons or the launch of this course · Kept until: you ask us to remove it. We never use it for anything else or share it. Reply to any notice email and we'll delete it right away.

Questions & answers

Ask anything about this lesson. Questions from every language version of this page appear together.

Shown as plain text, without links or formatting.

R

ReflexKR

Learn Python and Reflex by doing. Every example was run and verified on Reflex 0.10.0.

ReflexKR is free, with no banner ads. If it helped you, you can support it with a coffee. ☕ Buy us a coffee

Affiliate disclosure
한국어English日本語简体中文EspañolDeutsch

© 2026 ReflexKR · Since 2026 · CC BY-NC 4.0

Built with Reflex