By the end of this lesson, a horizontal bar chart of customers per gender will appear at the bottom of the card, and the bars will grow each time you add a customer. Shaping data in Python and showing it on screen is the basic pattern behind every data dashboard.
Recharts (the React chart library that Reflex wraps for you) takes a list of dictionaries. Each dictionary is one bar.
# the data we have # the data the chart wants
[User(..., gender="Male"), [{"name": "Male", "value": 1},
User(..., gender="Female")] → {"name": "Female", "value": 1}]So before you can draw the chart, you need a transformation (aggregation) step.
from collections import Counter
class State(rx.State):
users: list[User] = [...]
users_for_graph: list[dict] = [] # data for the chart
add_user_dialog_open: bool = False
@rx.event
def transform_data(self):
gender_counts = Counter(user.gender for user in self.users)
self.users_for_graph = [
{"name": gender_group, "value": count}
for gender_group, count in gender_counts.items()
]Counter is part of the Python standard library: Counter(["Male", "Female", "Male"]) → {"Male": 2, "Female": 1}.users_for_graph starts out as an empty list. You need to fill it at two points.
When the page opens: register the handler in add_page's on_load.
app.add_page(
index,
title="Customer Data App",
description="A simple app to manage customer data.",
on_load=State.transform_data, # runs every time the page is opened or refreshed
)When a customer is added: call it directly inside add_user.
@rx.event
def add_user(self, form_data: dict):
self.users.append(User(**form_data))
self.transform_data() # recompute the chart data
self.add_user_dialog_open = Falsedescription is the page description (a meta tag) shown by search engines and link previews.transform_data() everywhere you change users. If you add a delete feature and forget the call, the chart and table fall out of sync.With @rx.var, you can declare "a value computed from other state." Reflex recomputes it automatically whenever users changes.
class State(rx.State):
users: list[User] = [...]
@rx.var
def users_for_graph(self) -> list[dict]: # return type annotation required
counts = Counter(user.gender for user in self.users)
return [{"name": g, "value": c} for g, c in counts.items()]transform_data, on_load and the call inside add_user. The UI code (data=State.users_for_graph) stays the same.Instructor note — "Store a value and update it every time" vs. "declare how to compute it." A spreadsheet analogy works well: it's the difference between typing a value into a cell and using a formula (=COUNTIF(...)).
def graph():
return rx.recharts.bar_chart(
rx.recharts.cartesian_grid(
horizontal=False, stroke_dasharray="3 3", stroke=rx.color("slate", 4)
),
rx.recharts.x_axis(
type_="number",
allow_decimals=False,
axis_line=False,
tick_line=False,
domain=[0, "dataMax + 1"],
),
rx.recharts.y_axis(
type_="category",
data_key="name",
axis_line=False,
tick_line=False,
width=64,
),
rx.recharts.bar(
data_key="value",
fill=rx.color("accent", 9),
radius=[0, 6, 6, 0],
bar_size=28,
),
data=State.users_for_graph,
layout="vertical",
width="100%",
height=160,
margin={"top": 0, "right": 16, "bottom": 0, "left": 0},
)A chart is a component too. You pass its parts as children, and the data and size as keyword arguments.
| Part | Role | Key arguments |
|---|---|---|
bar_chart | The overall chart frame | data (list of dicts), layout="vertical" (horizontal bars) |
cartesian_grid | Background grid lines | horizontal=False (vertical lines only), stroke_dasharray="3 3" (dashed) |
x_axis | Horizontal axis = customer count | type_="number", allow_decimals=False (no ticks like 1.5 customers), domain=[0, "dataMax + 1"] (one extra unit past the max) |
y_axis | Vertical axis = gender | type_="category", data_key="name" (which dict key to use as the label) |
bar | The bars | data_key="value" (the key used for length), radius=[0, 6, 6, 0] (top-left, top-right, bottom-right, bottom-left corners; rounds only the end) |
layout="vertical", the x-axis is numeric and the y-axis is categorical. It's the opposite of vertical bars (the default), and it's the part people mix up most often.rx.color("accent", 9) is the theme's accent color. If you change it in rxconfig.py, for example RadixThemesPlugin(theme=rx.theme(accent_color="indigo")), the buttons and bars change color together.graph() as the card's third child, along with a title.rx.box(
rx.text("Audience breakdown", size="3", weight="bold"),
rx.text(
"Customers by gender",
size="2",
color=rx.color("slate", 11),
margin_top="4px",
margin_bottom="20px",
),
graph(),
padding="24px",
),Check your browser — An "Audience breakdown" section appears below the table, with Male and Female bars each reaching 1. The x-axis goes up to 2 because of domain=[0, "dataMax + 1"]. The bars animate in from the left, so give it 1–2 seconds.
localhost:3000
Try itYour customer directory and audience breakdown.
Audience breakdown
Customers by gender
In the preview above, add one Female customer: the Female bar grows to 2 and the x-axis widens to 3. If the chart doesn't change, check the self.transform_data() call inside add_user.
Once 5-5 works, switch from the event approach to a computed var while leaving the UI untouched. You'll change three places:
State, delete the users_for_graph: list[dict] = [] field and the transform_data method, and add the @rx.var method from 5-4. The names clash, so you must delete the field.self.transform_data() line inside add_user.on_load=State.transform_data from app.add_page.class State(rx.State):
users: list[User] = [...]
add_user_dialog_open: bool = False
@rx.event
def set_add_user_dialog_open(self, open_: bool):
self.add_user_dialog_open = open_
@rx.event
def add_user(self, form_data: dict):
self.users.append(User(**form_data))
self.add_user_dialog_open = False # no transform_data() call
@rx.var
def users_for_graph(self) -> list[dict]:
counts = Counter(user.gender for user in self.users)
return [{"name": g, "value": c} for g, c in counts.items()]
app = rx.App()
app.add_page(
index,
title="Customer Data App",
description="A simple app to manage customer data.",
) # no on_loaddata=State.users_for_graph in graph(). From the UI's point of view, a regular state var and a computed var look exactly the same.from collections import Counter as it is.Check your browser — After you save and hot reload kicks in, the chart looks exactly like it did in 5-5. Add a Female customer: if the bar grows to 2, the refactor worked. It updates without you calling anything, and that's the whole point of computed vars.
rx.recharts.graphing_tooltip() as a child of the chart so values show up on hover.@rx.var def female_count(self) -> int and show it as "Female: N" next to the chart title.@).on_load approach and the computed var approach.← Previous
Lesson 4 · Events and forms: adding customers
Next →
Lesson 6 · Wrap-up, debugging and next steps
Get news about new lessons
We'll email you when this course gets new lessons or a follow-up course opens.
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© 2026 ReflexKR · Since 2026 · CC BY-NC 4.0
Questions & answers
Ask anything about this lesson. Questions from every language version of this page appear together.