Files
StreamLens/analyzer/tui/textual/widgets/metric_card.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

140 lines
3.7 KiB
Python

"""
Metric Card Widget - Compact metric display inspired by TipTop
"""
from textual.widget import Widget
from textual.reactive import reactive
from rich.text import Text
from rich.console import RenderableType
from rich.panel import Panel
from typing import Optional, Literal
ColorType = Literal["normal", "success", "warning", "error"]
TrendType = Literal["up", "down", "stable"]
class MetricCard(Widget):
"""
Compact metric display card with optional sparkline
Features:
- Title and value display
- Color coding for status
- Optional trend indicator
- Optional inline sparkline
"""
DEFAULT_CSS = """
MetricCard {
width: 1fr;
height: 3;
margin: 0 1;
}
MetricCard.success {
border: solid $success;
}
MetricCard.warning {
border: solid $warning;
}
MetricCard.error {
border: solid $error;
}
"""
value = reactive("0")
color = reactive("normal")
def __init__(
self,
title: str,
value: str = "0",
color: ColorType = "normal",
trend: Optional[TrendType] = None,
sparkline: bool = False,
**kwargs
):
super().__init__(**kwargs)
self.title = title
self.value = value
self.color = color
self.trend = trend
self.sparkline = sparkline
self.spark_data = []
def update_value(self, new_value: str) -> None:
"""Update the metric value"""
self.value = new_value
def update_color(self, new_color: ColorType) -> None:
"""Update the color status"""
self.color = new_color
self.add_class(new_color)
def add_spark_data(self, value: float) -> None:
"""Add data point for sparkline"""
self.spark_data.append(value)
if len(self.spark_data) > 10: # Keep last 10 points
self.spark_data.pop(0)
def render(self) -> RenderableType:
"""Render the metric card"""
# Determine color style
color_map = {
"normal": "white",
"success": "green",
"warning": "yellow",
"error": "red"
}
style = color_map.get(self.color, "white")
# Create trend indicator
trend_icon = ""
if self.trend:
trend_map = {
"up": "",
"down": "",
"stable": ""
}
trend_icon = f" {trend_map.get(self.trend, '')}"
# Create sparkline if enabled
spark_str = ""
if self.sparkline and self.spark_data:
spark_str = " " + self._create_mini_spark()
# Format content
content = Text()
content.append(f"{self.title}\n", style="dim")
content.append(f"{self.value}", style=f"bold {style}")
content.append(trend_icon, style=style)
content.append(spark_str, style="dim cyan")
return Panel(
content,
height=3,
border_style=style if self.color != "normal" else "dim"
)
def _create_mini_spark(self) -> str:
"""Create mini sparkline for inline display"""
if not self.spark_data:
return ""
spark_chars = " ▁▂▃▄▅▆▇█"
data_min = min(self.spark_data)
data_max = max(self.spark_data)
if data_max == data_min:
return "" * len(self.spark_data)
result = []
for value in self.spark_data:
normalized = (value - data_min) / (data_max - data_min)
char_index = int(normalized * 8)
result.append(spark_chars[char_index])
return "".join(result)