- Fixed DataTable row selection and event handling - Added explicit column keys to prevent auto-generated keys - Implemented row-to-flow mapping for reliable selection tracking - Converted left metrics panel to horizontal top bar - Fixed all missing FlowStats/EnhancedAnalysisData attributes - Created comprehensive Textual API documentation in Documentation/textual/ - Added validation checklist to prevent future API mismatches - Preserved cursor position during data refreshes - Fixed RowKey type handling and event names The TUI now properly handles flow selection, displays metrics in a compact top bar, and correctly correlates selected rows with the details pane.
124 lines
3.4 KiB
Python
124 lines
3.4 KiB
Python
"""
|
|
Sparkline Widget - TipTop-style mini charts for real-time metrics
|
|
"""
|
|
|
|
from textual.widget import Widget
|
|
from textual.reactive import reactive
|
|
from typing import List, Optional
|
|
from rich.text import Text
|
|
from rich.console import RenderableType
|
|
from rich.panel import Panel
|
|
|
|
|
|
class SparklineWidget(Widget):
|
|
"""
|
|
ASCII sparkline chart widget inspired by TipTop
|
|
|
|
Shows trend visualization using Unicode block characters:
|
|
▁▂▃▄▅▆▇█
|
|
"""
|
|
|
|
DEFAULT_CSS = """
|
|
SparklineWidget {
|
|
height: 4;
|
|
padding: 0 1;
|
|
}
|
|
"""
|
|
|
|
data = reactive([], always_update=True)
|
|
|
|
def __init__(
|
|
self,
|
|
title: str,
|
|
data: List[float] = None,
|
|
height: int = 4,
|
|
color: str = "cyan",
|
|
**kwargs
|
|
):
|
|
super().__init__(**kwargs)
|
|
self.title = title
|
|
self.data = data or []
|
|
self.height = height
|
|
self.color = color
|
|
self.spark_chars = " ▁▂▃▄▅▆▇█"
|
|
|
|
def update_data(self, new_data: List[float]) -> None:
|
|
"""Update sparkline data"""
|
|
self.data = new_data
|
|
|
|
def render(self) -> RenderableType:
|
|
"""Render the sparkline chart"""
|
|
if not self.data:
|
|
return Panel(
|
|
f"{self.title}: No data",
|
|
height=self.height,
|
|
border_style="dim"
|
|
)
|
|
|
|
# Calculate sparkline
|
|
sparkline = self._create_sparkline()
|
|
|
|
# Get current value and trend
|
|
current = self.data[-1] if self.data else 0
|
|
trend = self._calculate_trend()
|
|
|
|
# Format current value
|
|
if self.title == "Flow Rate":
|
|
current_str = f"{current:.0f} flows"
|
|
elif self.title == "Packet Rate":
|
|
current_str = f"{current:.1f} pps"
|
|
else:
|
|
current_str = f"{current:.1f}"
|
|
|
|
# Create content
|
|
lines = [
|
|
f"{self.title}: {current_str} {trend}",
|
|
"",
|
|
sparkline
|
|
]
|
|
|
|
return Panel(
|
|
"\n".join(lines),
|
|
height=self.height,
|
|
border_style=self.color
|
|
)
|
|
|
|
def _create_sparkline(self) -> str:
|
|
"""Create sparkline visualization"""
|
|
if len(self.data) < 2:
|
|
return "─" * 40
|
|
|
|
# Normalize data
|
|
data_min = min(self.data)
|
|
data_max = max(self.data)
|
|
data_range = data_max - data_min
|
|
|
|
if data_range == 0:
|
|
# All values are the same
|
|
return "─" * min(len(self.data), 40)
|
|
|
|
# Create sparkline
|
|
sparkline_chars = []
|
|
for value in self.data[-40:]: # Last 40 values
|
|
# Normalize to 0-8 range (9 spark characters)
|
|
normalized = (value - data_min) / data_range
|
|
char_index = int(normalized * 8)
|
|
sparkline_chars.append(self.spark_chars[char_index])
|
|
|
|
return "".join(sparkline_chars)
|
|
|
|
def _calculate_trend(self) -> str:
|
|
"""Calculate trend indicator"""
|
|
if len(self.data) < 2:
|
|
return ""
|
|
|
|
# Compare last value to average of previous 5
|
|
current = self.data[-1]
|
|
prev_avg = sum(self.data[-6:-1]) / min(5, len(self.data) - 1)
|
|
|
|
if current > prev_avg * 1.1:
|
|
return "↑"
|
|
elif current < prev_avg * 0.9:
|
|
return "↓"
|
|
else:
|
|
return "→" |