Creating a Sophisticated Interactive Dashboard with Textual
This guide walks you through building a feature-rich, interactive dashboard using the Textual framework, showcasing how terminal-based UI libraries can rival the expressiveness and responsiveness of contemporary web dashboards. By incrementally developing the interface-adding widgets, layouts, reactive states, and event handlers-you’ll observe how Textual operates as a live UI engine, even within environments like Google Colab. By the end, you’ll appreciate how seamlessly tables, trees, forms, and progress bars integrate into a unified, fast, and elegant application.
Setting Up the Environment and Crafting Reactive Components
First, we prepare our workspace by installing necessary packages and importing essential modules to build our Textual app. A key reusable component, StatsCard, is designed to automatically update its display when its underlying value changes, demonstrating Textual’s powerful reactive system that simplifies dynamic UI creation.
!pip install textual textual-web nest-asyncio
from textual.app import App, ComposeResult
from textual.containers import Container, Horizontal, Vertical
from textual.widgets import Header, Footer, Button, DataTable, Static, Input, Label, ProgressBar, Tree, Select
from textual.reactive import reactive
from textual import on
from datetime import datetime
import random
class StatsCard(Static):
value = reactive(0)
def init(self, title: str, args, kwargs):
super().init(args, kwargs)
self.title = title
def compose(self) -> ComposeResult:
yield Label(self.title)
yield Label(str(self.value), id="stat-value")
def watchvalue(self, newvalue: int) -> None:
if self.ismounted:
try:
self.queryone("#stat-value", Label).update(str(newvalue))
except Exception:
pass
Defining the Dashboard Structure and Styling
Next, we create the DataDashboard class, where global styles, keyboard shortcuts, and reactive properties are configured. This approach gives us full control over the dashboard’s appearance and behavior without needing HTML or JavaScript, enabling a clean and maintainable design.
class DataDashboard(App):
CSS = """
Screen { background: $surface; }
#main-container { height: 100%; padding: 1; }
#stats-row { height: auto; margin-bottom: 1; }
StatsCard { border: solid $primary; height: 5; padding: 1; margin-right: 1; width: 1fr; }
#stat-value { text-style: bold; color: $accent; content-align: center middle; }
#control-panel { height: 12; border: solid $secondary; padding: 1; margin-bottom: 1; }
#data-section { height: 1fr; }
#left-panel { width: 30; border: solid $secondary; padding: 1; margin-right: 1; }
DataTable { height: 100%; border: solid $primary; }
Input { margin: 1 0; }
Button { margin: 1 1 1 0; }
ProgressBar { margin: 1 0; }
"""
BINDINGS = [
("d", "toggledark", "Toggle Dark Mode"),
("q", "quit", "Quit"),
("a", "addrow", "Add Row"),
("c", "cleartable", "Clear Table"),
]
totalrows = reactive(0)
totalsales = reactive(0)
avgrating = reactive(0.0)
Constructing the User Interface Layout
We then assemble the dashboard’s visual components, organizing containers, statistic cards, input fields, buttons, a navigation tree, and a data table. This declarative layout design allows us to visualize the dashboard’s structure clearly and intuitively.
def compose(self) -> ComposeResult:
yield Header(showclock=True)
with Container(id="main-container"):
with Horizontal(id="stats-row"):
yield StatsCard("Total Rows", id="card-rows")
yield StatsCard("Total Sales", id="card-sales")
yield StatsCard("Avg Rating", id="card-rating")
with Vertical(id="control-panel"):
yield Input(placeholder="Product Name", id="input-name")
yield Select(
[("Electronics", "electronics"),
("Books", "books"),
("Clothing", "clothing")],
prompt="Select Category",
id="select-category"
)
with Horizontal():
yield Button("Add Row", variant="primary", id="btn-add")
yield Button("Clear Table", variant="warning", id="btn-clear")
yield Button("Generate Data", variant="success", id="btn-generate")
yield ProgressBar(total=100, id="progress")
with Horizontal(id="data-section"):
with Container(id="left-panel"):
yield Label("Navigation")
tree = Tree("Dashboard")
tree.root.expand()
products = tree.root.add("Products", expand=True)
products.addleaf("Electronics")
products.addleaf("Books")
products.addleaf("Clothing")
tree.root.addleaf("Reports")
tree.root.addleaf("Settings")
yield tree
yield DataTable(id="data-table")
yield Footer()
Implementing Data Handling and Dynamic Updates
We add the core logic to populate the data table with sample entries, calculate summary statistics, and animate the progress bar. Textual’s reactive framework allows backend data changes to instantly reflect in the UI, creating a lively and responsive experience.
def onmount(self) -> None:
table = self.queryone(DataTable)
table.addcolumns("ID", "Product", "Category", "Price", "Sales", "Rating")
table.cursortype = "row"
self.generatesampledata(5)
self.setinterval(0.1, self.updateprogress)
def generatesampledata(self, count: int = 5) -> None:
table = self.queryone(DataTable)
categories = ["Electronics", "Books", "Clothing"]
products = {
"Electronics": ["Laptop", "Smartwatch", "Camera", "Speaker"],
"Books": ["Biography", "Science Fiction", "Cookbook", "Travel Guide"],
"Clothing": ["T-Shirt", "Jeans", "Jacket", "Sneakers"]
}
for in range(count):
category = random.choice(categories)
product = random.choice(products[category])
rowid = self.totalrows + 1
price = round(random.uniform(15, 600), 2)
sales = random.randint(5, 150)
rating = round(random.uniform(1, 5), 1)
table.addrow(
str(rowid),
product,
category,
f"${price}",
str(sales),
str(rating)
)
self.totalrows += 1
self.totalsales += sales
self.updatestats()
def updatestats(self) -> None:
self.queryone("#card-rows", StatsCard).value = self.totalrows
self.queryone("#card-sales", StatsCard).value = self.totalsales
if self.totalrows > 0:
table = self.queryone(DataTable)
totalrating = sum(float(row[5]) for row in table.rows)
self.avgrating = round(totalrating / self.totalrows, 2)
self.queryone("#card-rating", StatsCard).value = self.avgrating
def updateprogress(self) -> None:
progress = self.queryone(ProgressBar)
progress.advance(1)
if progress.progress >= 100:
progress.progress = 0
Linking User Interactions to Application Logic
Finally, we bind UI events such as button presses and keyboard shortcuts to their corresponding backend functions. This integration ensures that user actions like adding rows, clearing data, or toggling themes immediately affect the dashboard’s state, delivering a smooth and interactive user experience.
@on(Button.Pressed, "#btn-add")
def handleaddbutton(self) -> None:
nameinput = self.queryone("#input-name", Input)
category = self.queryone("#select-category", Select).value
if nameinput.value and category:
table = self.queryone(DataTable)
rowid = self.totalrows + 1
price = round(random.uniform(15, 600), 2)
sales = random.randint(5, 150)
rating = round(random.uniform(1, 5), 1)
table.addrow(
str(rowid),
nameinput.value,
str(category),
f"${price}",
str(sales),
str(rating)
)
self.totalrows += 1
self.totalsales += sales
self.updatestats()
nameinput.value = ""
@on(Button.Pressed, "#btn-clear")
def handleclearbutton(self) -> None:
table = self.queryone(DataTable)
table.clear()
self.totalrows = 0
self.totalsales = 0
self.avgrating = 0
self.updatestats()
@on(Button.Pressed, "#btn-generate")
def handlegeneratebutton(self) -> None:
self.generatesampledata(10)
def actiontoggledark(self) -> None:
self.dark = not self.dark
def actionaddrow(self) -> None:
self.handleaddbutton()
def actioncleartable(self) -> None:
self.handleclearbutton()
if name == "main":
import nestasyncio
nestasyncio.apply()
app = DataDashboard()
app.run()
Summary
This tutorial demonstrates how to assemble a fully interactive, terminal-based dashboard that runs seamlessly within a notebook environment. Leveraging Textual’s modern reactive UI capabilities, we build a polished interface that combines data visualization, user input, and real-time updates-all using pure Python. This foundation can be expanded with additional features such as charting libraries, live API integrations, and multi-page navigation to create even more powerful terminal applications.
