ai-station/.venv/lib/python3.12/site-packages/textual/renderables/sparkline.py

127 lines
4.2 KiB
Python

from __future__ import annotations
import statistics
from fractions import Fraction
from typing import Callable, Generic, Iterable, Sequence, TypeVar
from rich.color import Color
from rich.console import Console, ConsoleOptions, RenderResult
from rich.measure import Measurement
from rich.segment import Segment
from rich.style import Style
from textual.renderables._blend_colors import blend_colors
T = TypeVar("T", int, float)
SummaryFunction = Callable[[Sequence[T]], float]
class Sparkline(Generic[T]):
"""A sparkline representing a series of data.
Args:
data: The sequence of data to render.
width: The width of the sparkline/the number of buckets to partition the data into.
min_color: The color of values equal to the min value in data.
max_color: The color of values equal to the max value in data.
summary_function: Function that will be applied to each bucket.
"""
BARS = "▁▂▃▄▅▆▇█"
def __init__(
self,
data: Sequence[T],
*,
width: int | None,
min_color: Color = Color.from_rgb(0, 255, 0),
max_color: Color = Color.from_rgb(255, 0, 0),
summary_function: SummaryFunction[T] = max,
) -> None:
self.data: Sequence[T] = data
self.width = width
self.min_color = Style.from_color(min_color)
self.max_color = Style.from_color(max_color)
self.summary_function: SummaryFunction[T] = summary_function
@classmethod
def _buckets(cls, data: list[T], num_buckets: int) -> Iterable[Sequence[T]]:
"""Partition ``data`` into ``num_buckets`` buckets. For example, the data
[1, 2, 3, 4] partitioned into 2 buckets is [[1, 2], [3, 4]].
Args:
data: The data to partition.
num_buckets: The number of buckets to partition the data into.
"""
bucket_step = Fraction(len(data), num_buckets)
for bucket_no in range(num_buckets):
start = int(bucket_step * bucket_no)
end = int(bucket_step * (bucket_no + 1))
partition = data[start:end]
if partition:
yield partition
def __rich_console__(
self, console: Console, options: ConsoleOptions
) -> RenderResult:
width = self.width or options.max_width
len_data = len(self.data)
if len_data == 0:
yield Segment("" * width, self.min_color)
return
if len_data == 1:
yield Segment("" * width, self.max_color)
return
minimum, maximum = min(self.data), max(self.data)
extent = maximum - minimum or 1
buckets = tuple(self._buckets(list(self.data), num_buckets=width))
bucket_index = 0.0
bars_rendered = 0
step = len(buckets) / width
summary_function = self.summary_function
min_color, max_color = self.min_color.color, self.max_color.color
assert min_color is not None
assert max_color is not None
while bars_rendered < width:
partition = buckets[int(bucket_index)]
partition_summary = summary_function(partition)
height_ratio = (partition_summary - minimum) / extent
bar_index = int(height_ratio * (len(self.BARS) - 1))
bar_color = blend_colors(min_color, max_color, height_ratio)
bars_rendered += 1
bucket_index += step
yield Segment(self.BARS[bar_index], Style.from_color(bar_color))
def __rich_measure__(
self, console: "Console", options: "ConsoleOptions"
) -> Measurement:
return Measurement(self.width or options.max_width, 1)
if __name__ == "__main__":
console = Console()
def last(l: Sequence[T]) -> T:
return l[-1]
funcs: Sequence[SummaryFunction[int]] = (
min,
max,
last,
statistics.median,
statistics.mean,
)
nums = [10, 2, 30, 60, 45, 20, 7, 8, 9, 10, 50, 13, 10, 6, 5, 4, 3, 7, 20]
console.print(f"data = {nums}\n")
for f in funcs:
console.print(
f"{f.__name__}:\t",
Sparkline(nums, width=12, summary_function=f),
end="",
)
console.print("\n")