Charts¶
Castella provides native interactive chart widgets with GPU rendering via Skia (desktop) and CanvasKit (web). No external dependencies like Matplotlib required.
Features¶
- 8 Chart Types: Bar, Line, Pie, Scatter, Area, Stacked Bar, Gauge, Heatmap
- Full Interactivity: Tooltips, hover effects, click events
- Drill-Down Navigation: Click to explore hierarchical data (Region → Country → City)
- Scientific Colormaps: Viridis, Plasma, Inferno, Magma (colorblind-friendly)
- Pydantic v2 Models: Type-safe, validated data models
- Theme Integration: Automatically uses current Castella theme
- Observable Pattern: Charts auto-update when data changes
Quick Start¶
from castella.chart import BarChart, CategoricalChartData, CategoricalSeries, SeriesStyle
# Create data
data = CategoricalChartData(title="Sales by Quarter")
data.add_series(CategoricalSeries.from_values(
name="2024",
categories=["Q1", "Q2", "Q3", "Q4"],
values=[100, 120, 90, 150],
style=SeriesStyle(color="#3b82f6"),
))
# Create chart
chart = BarChart(data, show_values=True)
Data Models¶
CategoricalChartData¶
For charts with category labels (Bar, Pie, Stacked Bar):
from castella.chart import CategoricalChartData, CategoricalSeries, SeriesStyle
data = CategoricalChartData(title="Monthly Sales")
data.add_series(CategoricalSeries.from_values(
name="Product A",
categories=["Jan", "Feb", "Mar", "Apr"],
values=[50, 75, 60, 90],
style=SeriesStyle(color="#3b82f6"),
))
data.add_series(CategoricalSeries.from_values(
name="Product B",
categories=["Jan", "Feb", "Mar", "Apr"],
values=[40, 55, 70, 65],
style=SeriesStyle(color="#22c55e"),
))
NumericChartData¶
For charts with numeric X/Y data (Line, Scatter, Area):
from castella.chart import NumericChartData, NumericSeries, SeriesStyle
data = NumericChartData(title="Temperature Over Time")
# Y values only (X auto-generated as 0, 1, 2, ...)
data.add_series(NumericSeries.from_y_values(
name="Sensor A",
y_values=[20, 22, 21, 25, 24, 23],
style=SeriesStyle(color="#3b82f6"),
))
# Explicit X and Y values
data.add_series(NumericSeries.from_values(
name="Sensor B",
x_values=[0, 1, 2, 3, 4, 5],
y_values=[18, 19, 20, 22, 21, 20],
style=SeriesStyle(color="#22c55e"),
))
GaugeChartData¶
For gauge/meter charts:
from castella.chart import GaugeChartData
data = GaugeChartData(
title="CPU Usage",
value=67,
min_value=0,
max_value=100,
value_format="{:.0f}%",
thresholds=[
(0.0, "#22c55e"), # Green for 0-50%
(0.5, "#f59e0b"), # Yellow for 50-80%
(0.8, "#ef4444"), # Red for 80-100%
],
)
HeatmapChartData¶
For heatmap charts with 2D matrix data:
from castella.chart import HeatmapChartData
# Create from 2D array
data = HeatmapChartData.from_2d_array(
values=[
[1.0, 0.8, 0.3],
[0.8, 1.0, 0.5],
[0.3, 0.5, 1.0],
],
row_labels=["A", "B", "C"],
column_labels=["X", "Y", "Z"],
title="Correlation Matrix",
)
# Set value range for color normalization
data.set_range(min_value=-1.0, max_value=1.0)
# Update single cell
data.set_cell(row=0, col=1, value=0.9)
# Update entire matrix
data.set_values([[1.0, 0.9], [0.9, 1.0]])
Chart Types¶
BarChart¶
Vertical bar chart for categorical data:
from castella.chart import BarChart, CategoricalChartData, CategoricalSeries
data = CategoricalChartData(title="Sales")
data.add_series(CategoricalSeries.from_values(
name="Revenue",
categories=["Q1", "Q2", "Q3", "Q4"],
values=[100, 150, 120, 180],
))
chart = BarChart(
data,
show_values=True, # Show value labels on bars
bar_width_ratio=0.7, # Bar width as ratio of available space
enable_tooltip=True, # Show tooltips on hover
)
LineChart¶
Line chart with optional points:
from castella.chart import LineChart, NumericChartData, NumericSeries
data = NumericChartData(title="Stock Price")
data.add_series(NumericSeries.from_y_values(
name="AAPL",
y_values=[150, 155, 148, 160, 158, 165],
))
chart = LineChart(
data,
show_points=True, # Show data points
point_radius=4.0, # Point size
line_width=2.0, # Line thickness
smooth=False, # Use straight lines (True for Catmull-Rom splines)
)
# Smooth curves using Catmull-Rom spline interpolation
smooth_chart = LineChart(data, smooth=True)
PieChart¶
Pie and donut charts:
from castella.chart import PieChart, CategoricalChartData, CategoricalSeries
data = CategoricalChartData(title="Market Share")
data.add_series(CategoricalSeries.from_values(
name="Companies",
categories=["Company A", "Company B", "Company C", "Others"],
values=[35, 28, 22, 15],
))
chart = PieChart(
data,
inner_radius_ratio=0.0, # 0.0 for pie, 0.5+ for donut
show_labels=True, # Show category labels
show_percentages=True, # Show percentage values
)
ScatterChart¶
Scatter plot with customizable point shapes:
from castella.chart import ScatterChart, PointShape, NumericChartData, NumericSeries
data = NumericChartData(title="Height vs Weight")
data.add_series(NumericSeries.from_values(
name="Male",
x_values=[170, 175, 180, 185],
y_values=[70, 75, 80, 85],
))
chart = ScatterChart(
data,
point_radius=6,
point_shape=PointShape.CIRCLE, # CIRCLE, SQUARE, DIAMOND, TRIANGLE
show_grid=True,
)
AreaChart¶
Filled area chart:
from castella.chart import AreaChart, NumericChartData, NumericSeries
data = NumericChartData(title="Website Traffic")
data.add_series(NumericSeries.from_y_values(
name="Visitors",
y_values=[1200, 1500, 1300, 1800, 2100],
))
chart = AreaChart(
data,
fill_opacity=0.3, # Area fill transparency
show_points=True, # Show data points
stacked=False, # Stack multiple series
)
StackedBarChart¶
Stacked bar chart for comparing parts of a whole:
from castella.chart import StackedBarChart, CategoricalChartData, CategoricalSeries
data = CategoricalChartData(title="Revenue by Region")
data.add_series(CategoricalSeries.from_values(
name="North", categories=["Q1", "Q2", "Q3", "Q4"], values=[100, 120, 90, 150],
))
data.add_series(CategoricalSeries.from_values(
name="South", categories=["Q1", "Q2", "Q3", "Q4"], values=[80, 100, 110, 95],
))
data.add_series(CategoricalSeries.from_values(
name="East", categories=["Q1", "Q2", "Q3", "Q4"], values=[60, 75, 85, 70],
))
chart = StackedBarChart(
data,
normalized=False, # True for 100% stacked chart
show_values=False,
)
GaugeChart¶
Gauge/meter chart with threshold colors:
from castella.chart import GaugeChart, GaugeStyle, GaugeChartData
data = GaugeChartData(
title="CPU Usage",
value=67,
min_value=0,
max_value=100,
value_format="{:.0f}%",
thresholds=[
(0.0, "#22c55e"), # Green
(0.5, "#f59e0b"), # Yellow
(0.8, "#ef4444"), # Red
],
)
chart = GaugeChart(
data,
style=GaugeStyle.HALF_CIRCLE, # HALF_CIRCLE, THREE_QUARTER, FULL_CIRCLE
show_ticks=True,
arc_width=25,
)
HeatmapChart¶
Heatmap for 2D matrix data visualization:
from castella.chart import HeatmapChart, HeatmapChartData, ColormapType
# Create correlation matrix data
data = HeatmapChartData.from_2d_array(
values=[
[1.00, 0.85, 0.42, -0.15],
[0.85, 1.00, 0.55, 0.08],
[0.42, 0.55, 1.00, 0.67],
[-0.15, 0.08, 0.67, 1.00],
],
row_labels=["Price", "Volume", "Volatility", "Beta"],
column_labels=["Price", "Volume", "Volatility", "Beta"],
title="Correlation Matrix",
)
# Set fixed value range for color normalization
data.set_range(-1.0, 1.0)
chart = HeatmapChart(
data,
colormap=ColormapType.VIRIDIS, # VIRIDIS, PLASMA, INFERNO, MAGMA
show_values=True, # Show value annotations in cells
show_colorbar=True, # Show color bar legend
cell_gap=2.0, # Gap between cells in pixels
)
Colormaps¶
Castella provides scientific colormaps that are perceptually uniform and colorblind-friendly:
from castella.chart import viridis, plasma, inferno, magma, get_colormap
# Get colormap by type
cmap = get_colormap(ColormapType.VIRIDIS)
# Get color for a normalized value (0.0 to 1.0)
color = cmap(0.5) # Returns "#26828e"
# Get N evenly spaced colors
colors = cmap.get_colors(5) # ["#440154", "#3e4a89", "#26828e", "#35b779", "#fde725"]
# Reversed colormap
reversed_cmap = cmap.reversed()
Heatmap with DataTable¶
You can also apply heatmap coloring to DataTable cells using HeatmapConfig:
from castella import DataTable, DataTableState, ColumnConfig, HeatmapConfig
from castella.chart import ColormapType
# Create table data
state = DataTableState(
columns=[
ColumnConfig(name="Region", width=120),
ColumnConfig(name="Q1", width=80),
ColumnConfig(name="Q2", width=80),
ColumnConfig(name="Q3", width=80),
ColumnConfig(name="Q4", width=80),
],
rows=[
["North America", 145, 162, 178, 195],
["Europe", 122, 138, 155, 168],
["Asia Pacific", 198, 215, 242, 267],
],
)
# Apply heatmap coloring to numeric columns
heatmap = HeatmapConfig(colormap=ColormapType.VIRIDIS)
for i in range(1, 5):
state.columns[i].cell_bg_color = heatmap.create_color_fn(col_idx=i, state=state)
table = DataTable(state)
Features:
- Auto-contrast text: Text color automatically adjusts for readability
- Per-column ranges: Each column can have its own value range
- Custom colormaps: Pass any Colormap instance
Interactivity¶
Event Handlers¶
All charts support hover and click events:
chart = BarChart(data).on_click(on_bar_click).on_hover(on_bar_hover)
def on_bar_click(event):
print(f"Clicked: {event.label} = {event.value}")
print(f"Series: {event.series_index}, Data: {event.data_index}")
def on_bar_hover(event):
print(f"Hovering: {event.label}")
Tooltips¶
Tooltips are enabled by default and show on hover:
chart = LineChart(data, enable_tooltip=True) # Default
chart = LineChart(data, enable_tooltip=False) # Disable
Series Visibility¶
Toggle series visibility (useful with legends):
# Hide/show series
data.set_series_visibility(series_index=1, visible=False)
data.toggle_series_visibility(series_index=0)
# Check visibility
if data.is_series_visible(0):
print("Series 0 is visible")
Reactive Updates¶
Charts automatically re-render when their data changes:
class LiveChart(Component):
def __init__(self):
super().__init__()
self._data = GaugeChartData(title="CPU", value=50, max_value=100)
self._data.attach(self) # Re-render on data change
def view(self):
return Column(
GaugeChart(self._data, style=GaugeStyle.HALF_CIRCLE),
Button("Update").on_click(self._update),
)
def _update(self, _):
import random
self._data.set_value(random.randint(0, 100)) # Triggers re-render
SeriesStyle¶
Customize series appearance:
from castella.chart import SeriesStyle
style = SeriesStyle(
color="#3b82f6", # Primary color
fill_opacity=0.3, # Fill transparency (for area charts)
line_width=2.0, # Line thickness
point_radius=4.0, # Point size
)
series = CategoricalSeries.from_values(
name="Sales",
categories=["A", "B", "C"],
values=[10, 20, 30],
style=style,
)
Complete Example¶
from castella import App, Component, Column, Row, Button
from castella.frame import Frame
from castella.chart import (
BarChart, LineChart, PieChart, GaugeChart, GaugeStyle,
CategoricalChartData, NumericChartData, GaugeChartData,
CategoricalSeries, NumericSeries, SeriesStyle,
)
class ChartDemo(Component):
def __init__(self):
super().__init__()
# Bar chart data
self._bar_data = CategoricalChartData(title="Quarterly Sales")
self._bar_data.add_series(CategoricalSeries.from_values(
name="2024",
categories=["Q1", "Q2", "Q3", "Q4"],
values=[120, 150, 130, 180],
style=SeriesStyle(color="#3b82f6"),
))
self._bar_data.attach(self)
# Line chart data
self._line_data = NumericChartData(title="Daily Views")
self._line_data.add_series(NumericSeries.from_y_values(
name="Page Views",
y_values=[100, 120, 90, 150, 180, 160, 200],
style=SeriesStyle(color="#22c55e"),
))
self._line_data.attach(self)
# Gauge data
self._gauge_data = GaugeChartData(
title="Performance",
value=75,
max_value=100,
value_format="{:.0f}%",
)
self._gauge_data.attach(self)
def view(self):
return Column(
Row(
BarChart(self._bar_data, show_values=True)
.on_click(lambda e: print(f"Bar: {e.label}")),
LineChart(self._line_data, show_points=True)
.on_hover(lambda e: print(f"Line: {e.value}")),
),
Row(
GaugeChart(self._gauge_data, style=GaugeStyle.HALF_CIRCLE),
Button("Randomize").on_click(self._randomize),
).fixed_height(200),
)
def _randomize(self, _):
import random
self._gauge_data.set_value(random.randint(0, 100))
App(Frame("Chart Demo", 1000, 700), ChartDemo()).run()
Drill-Down Charts¶
Drill-down charts allow users to navigate through hierarchical data by clicking on data points to see more detailed breakdowns.
Supported Chart Types¶
- BarChart: Click bars to drill down
- PieChart: Click slices to drill down
- StackedBarChart: Click stacked segments to drill down
- HeatmapChart: Click cells (row-based) to drill down
Basic Usage¶
from castella.chart import (
DrillDownChart, HierarchicalChartData, HierarchicalNode,
DataPoint, BarChart,
)
# Create hierarchical data: Region -> Country -> City
data = HierarchicalChartData(
title="Global Sales",
root=HierarchicalNode(
id="world",
label="World",
level_name="Region",
data=[
DataPoint(category="North America", value=1500, label="North America"),
DataPoint(category="Europe", value=1200, label="Europe"),
DataPoint(category="Asia", value=1800, label="Asia"),
],
),
)
# Add child data for North America
na_node = HierarchicalNode(
id="na",
label="North America",
level_name="Country",
data=[
DataPoint(category="USA", value=900, label="USA"),
DataPoint(category="Canada", value=400, label="Canada"),
DataPoint(category="Mexico", value=200, label="Mexico"),
],
)
data.root.add_child("North America", na_node)
# Create drill-down chart
chart = (
DrillDownChart(data, chart_type=BarChart, show_values=True)
.on_drill_down(lambda ev: print(f"Drilled into: {ev.clicked_key}"))
.on_drill_up(lambda ev: print(f"Drilled up to: {ev.to_node_id}"))
)
Navigation Controls¶
- Click on data: Drill down into child data
- Back button: Go up one level
- Breadcrumb: Navigate to any ancestor level
Multi-Series Data (StackedBarChart / HeatmapChart)¶
For StackedBarChart and HeatmapChart, use series_data instead of data:
from castella.chart import (
DrillDownChart, HierarchicalChartData, HierarchicalNode,
DataPoint, StackedBarChart, HeatmapChart,
)
def make_quarterly_data(categories, base_values):
"""Create quarterly breakdown for each category."""
quarters = ["Q1", "Q2", "Q3", "Q4"]
ratios = [0.2, 0.25, 0.25, 0.3]
result = {}
for q_idx, quarter in enumerate(quarters):
points = []
for cat, base in zip(categories, base_values):
value = base * ratios[q_idx]
points.append(DataPoint(category=cat, value=value, label=cat))
result[quarter] = points
return result
# Root level with quarterly breakdown
data = HierarchicalChartData(
title="Quarterly Sales by Region",
root=HierarchicalNode(
id="world",
label="World",
level_name="Region",
series_data=make_quarterly_data(
["North America", "Europe", "Asia"],
[1500, 1200, 1800],
),
),
)
# StackedBarChart shows stacked bars with Q1-Q4 colors
stacked_chart = DrillDownChart(data, chart_type=StackedBarChart, show_values=True)
# HeatmapChart shows 2D matrix (rows=categories, columns=quarters)
heatmap_chart = DrillDownChart(data, chart_type=HeatmapChart, show_values=True)
Time-Series Drill-Down¶
Helper function for Year → Month → Day hierarchy:
from datetime import date
from castella.chart import (
DrillDownChart, hierarchical_from_timeseries, BarChart,
)
# Sample data: (date, value) tuples
daily_sales = [
(date(2023, 1, 1), 120.0),
(date(2023, 1, 2), 145.0),
# ... more data
]
# Create hierarchical structure automatically
data = hierarchical_from_timeseries(
daily_sales,
title="Daily Sales",
aggregation="sum", # sum, avg, count, min, max
depth="day", # year, month, or day
short_month_names=True, # "Jan" vs "January"
value_format=lambda v: f"${v:,.0f}",
)
chart = DrillDownChart(data, chart_type=BarChart, show_values=True)
Programmatic Navigation¶
chart = DrillDownChart(data, chart_type=BarChart)
# Access the drill-down state
state = chart.state
# Navigate programmatically
state.drill_down("North America")
state.drill_up()
state.navigate_to("world") # Jump to specific node
state.reset() # Reset to root
# Check navigation state
print(state.current_depth) # 0 = root
print(state.can_drill_up) # True if not at root
print(state.breadcrumbs) # [(node_id, label), ...]
Visual Indicators¶
Drillable data points show a small white dot indicator:
- BarChart: Bottom-right corner of the bar
- PieChart: Inside the slice at 60% radius
- StackedBarChart: Bottom-right of each segment
- HeatmapChart: Left edge of the first cell in each drillable row
Example Application¶
from castella import App, Column, Text
from castella.core import Component, Widget
from castella.frame import Frame
from castella.chart import (
DrillDownChart, HierarchicalChartData, HierarchicalNode,
DataPoint, BarChart,
)
class SalesDashboard(Component):
def __init__(self):
super().__init__()
self._data = self._create_sales_data()
def _create_sales_data(self):
data = HierarchicalChartData(
title="Sales by Region",
root=HierarchicalNode(
id="world",
label="World",
level_name="Region",
data=[
DataPoint(category="Americas", value=2500),
DataPoint(category="EMEA", value=1800),
DataPoint(category="APAC", value=2200),
],
),
)
# Add children for Americas
data.root.add_child("Americas", HierarchicalNode(
id="americas",
label="Americas",
level_name="Country",
data=[
DataPoint(category="USA", value=1500),
DataPoint(category="Canada", value=600),
DataPoint(category="Brazil", value=400),
],
))
return data
def view(self) -> Widget:
return Column(
Text("Sales Dashboard", font_size=20).fixed_height(40),
DrillDownChart(
self._data,
chart_type=BarChart,
show_values=True,
),
)
App(Frame("Sales Dashboard", 800, 600), SalesDashboard()).run()
ASCII Charts (Terminal)¶
For terminal environments, Castella provides ASCII chart widgets that render using Unicode characters.
ASCIIBarChart¶
from castella.chart import ASCIIBarChart, ASCIIBarData
data = ASCIIBarData(
title="Quarterly Sales",
labels=["Q1", "Q2", "Q3", "Q4"],
values=[120, 180, 150, 200],
)
chart = ASCIIBarChart(data, width=30, show_values=True)
Output:
Quarterly Sales
Q1 │██████████████████ │ 120.0
Q2 │███████████████████████████ │ 180.0
Q3 │██████████████████████▌ │ 150.0
Q4 │██████████████████████████████│ 200.0
ASCIIPieChart¶
from castella.chart import ASCIIPieChart, ASCIIPieData
data = ASCIIPieData(
title="Market Share",
labels=["Product A", "Product B", "Others"],
values=[50, 30, 20],
)
chart = ASCIIPieChart(data)
Output:
ASCIILineChart¶
from castella.chart import ASCIILineChart
chart = ASCIILineChart(
values=[10, 25, 15, 30, 20, 35],
width=40,
height=8,
title="Trend",
)
ASCIIGaugeChart¶
from castella.chart import ASCIIGaugeChart
chart = ASCIIGaugeChart(
value=67.5,
max_value=100,
width=25,
title="CPU Usage",
)
Output:
Running in Terminal Mode¶
To run your app in terminal mode: