Files
StreamLens/analyzer/tui/textual/widgets/sparkline.py
noisedestroyers 36a576dc2c 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.
2025-07-27 18:37:55 -04:00

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 ""