Files
StreamLens/analyzer/tui/textual/widgets/metric_card.py
2025-07-28 08:14:15 -04:00

141 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;
padding: 0;
}
MetricCard.success {
border: none;
color: #00ff88;
}
MetricCard.warning {
border: none;
color: #ffcc00;
}
MetricCard.error {
border: none;
color: #ff3366;
}
"""
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()
# Ultra compact - single line format
content = Text()
content.append(f"{self.title}: ", style="dim")
content.append(f"{self.value}", style=f"bold {style}")
content.append(trend_icon, style=style)
content.append(spark_str, style="dim cyan")
# Super compact - no panel, just text
return content
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)