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