Enhanced Textual TUI with proper API usage and documentation
- 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.
This commit is contained in:
124
analyzer/tui/textual/widgets/sparkline.py
Normal file
124
analyzer/tui/textual/widgets/sparkline.py
Normal file
@@ -0,0 +1,124 @@
|
||||
"""
|
||||
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 "→"
|
||||
Reference in New Issue
Block a user