140 lines
3.7 KiB
Python
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)
|