mirror of
https://github.com/Textualize/rich.git
synced 2025-12-23 07:08:35 +00:00
examples: add bar_chart demos and outputs
This commit is contained in:
parent
a6cd41a7cb
commit
99e9fa65a8
8 changed files with 643 additions and 0 deletions
34
demo_after.py
Normal file
34
demo_after.py
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
"""Demo: behavior AFTER adding explicit markup validation.
|
||||
|
||||
This script validates the same broken markup string using
|
||||
`rich.markup_validator.MarkupValidator`. If validation fails a
|
||||
`rich.errors.MarkupError` is caught and a clear alert is printed.
|
||||
|
||||
Run with: python demo_after.py
|
||||
"""
|
||||
from rich.console import Console
|
||||
from rich.markup_validator import MarkupValidator
|
||||
from rich.errors import MarkupError
|
||||
|
||||
|
||||
broken_text = "[bold]Hello[/dim]"
|
||||
|
||||
|
||||
def main() -> None:
|
||||
console = Console()
|
||||
validator = MarkupValidator()
|
||||
|
||||
try:
|
||||
# Validate markup first. This will raise MarkupError for mismatches.
|
||||
validator.validate(broken_text)
|
||||
except MarkupError as exc:
|
||||
console.print("[bold red]🚨 Validation Failed![/]")
|
||||
console.print(f"[red]{exc}[/]")
|
||||
else:
|
||||
# Only print if validation passes
|
||||
console.print("-- Validation passed; now printing --")
|
||||
console.print(broken_text)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
30
demo_before.py
Normal file
30
demo_before.py
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
"""Demo: behavior BEFORE adding explicit markup validation.
|
||||
|
||||
This script prints a broken markup string directly with `rich.Console.print`.
|
||||
It demonstrates the "silent failure" behavior where mismatched tags do not
|
||||
raise an exception and the renderer may ignore or render them literally.
|
||||
|
||||
Run with: python demo_before.py
|
||||
"""
|
||||
from rich.console import Console
|
||||
|
||||
|
||||
broken_text = "[bold]Hello[/dim]"
|
||||
|
||||
|
||||
def main() -> None:
|
||||
console = Console()
|
||||
|
||||
# Intentionally printing a string with mismatched tags WITHOUT validation.
|
||||
# In this demo we do not use MarkupValidator, so Console.print will not raise
|
||||
# an exception; it will either ignore the bad closing tag or render it
|
||||
# depending on the environment. This illustrates a silent failure.
|
||||
console.print("-- Printing without validation (silent failure expected) --")
|
||||
console.print(broken_text)
|
||||
|
||||
# Also print a plain notification so the behavior is explicit when run
|
||||
print("Printed without validation — no exception thrown (silent failure).")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
12
demo_file_explorer.py
Normal file
12
demo_file_explorer.py
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
from rich.console import Console
|
||||
from rich.panel import Panel
|
||||
from rich.file_explorer import FileExplorer # 방금 만든 모듈
|
||||
|
||||
console = Console()
|
||||
|
||||
# 1. 현재 디렉토리(.) 탐색
|
||||
console.print(Panel(FileExplorer("."), title="📂 현재 프로젝트 구조", border_style="blue"))
|
||||
|
||||
# 2. 특정 폴더 무시하고 탐색 (예: __pycache__, .git)
|
||||
# ignore_list = [".git", "__pycache__", ".vscode"]
|
||||
# console.print(Panel(FileExplorer(".", ignore=ignore_list), title="Clean View", border_style="green"))
|
||||
0
examples-bar_output.txt
Normal file
0
examples-bar_output.txt
Normal file
105
examples/bar_chart.py
Normal file
105
examples/bar_chart.py
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
"""
|
||||
Example of using BarChart to display data.
|
||||
|
||||
Run this example with:
|
||||
python examples/bar_chart.py
|
||||
"""
|
||||
|
||||
from rich import print
|
||||
from rich.bar_chart import BarChart
|
||||
from rich.panel import Panel
|
||||
|
||||
# Example 1: Using a dictionary
|
||||
print("\n[bold]Example 1: Sales Data (Dictionary)[/bold]")
|
||||
sales_data = {
|
||||
"Q1": 45000,
|
||||
"Q2": 52000,
|
||||
"Q3": 48000,
|
||||
"Q4": 61000,
|
||||
}
|
||||
chart1 = BarChart(sales_data, width=50)
|
||||
print(chart1)
|
||||
|
||||
# Example 2: Using a list of tuples
|
||||
print("\n[bold]Example 2: Population by City (List of Tuples)[/bold]")
|
||||
population_data = [
|
||||
("Seoul", 9.7),
|
||||
("Busan", 3.4),
|
||||
("Incheon", 2.9),
|
||||
("Daegu", 2.4),
|
||||
("Daejeon", 1.5),
|
||||
]
|
||||
chart2 = BarChart(population_data, width=50, max_value=10.0)
|
||||
print(chart2)
|
||||
|
||||
# Example 3: Using custom styles
|
||||
print("\n[bold]Example 3: Custom Colors[/bold]")
|
||||
chart3 = BarChart(
|
||||
{"Apples": 30, "Oranges": 25, "Bananas": 20, "Grapes": 15},
|
||||
width=50,
|
||||
bar_styles=["red", "yellow", "green", "magenta"],
|
||||
label_style="bold",
|
||||
value_style="dim",
|
||||
)
|
||||
print(chart3)
|
||||
|
||||
# Example 4: Without values
|
||||
print("\n[bold]Example 4: Without Value Labels[/bold]")
|
||||
chart4 = BarChart(
|
||||
{"Jan": 100, "Feb": 150, "Mar": 120, "Apr": 180},
|
||||
width=50,
|
||||
show_values=False,
|
||||
style="cyan",
|
||||
)
|
||||
print(chart4)
|
||||
|
||||
# Example 5: In a Panel
|
||||
print("\n[bold]Example 5: Chart in a Panel[/bold]")
|
||||
chart5 = BarChart(
|
||||
{"Python": 85, "JavaScript": 75, "Java": 70, "C++": 65},
|
||||
width=50,
|
||||
bar_styles=["blue", "yellow", "red", "green"],
|
||||
)
|
||||
panel = Panel(chart5, title="Programming Language Popularity", border_style="blue")
|
||||
print(panel)
|
||||
|
||||
print("\n[bold]Example 6: Vertical Bar Chart[/bold]")
|
||||
vertical = BarChart(
|
||||
{"Jan": 3, "Feb": 7, "Mar": 5, "Apr": 9},
|
||||
orientation="vertical",
|
||||
chart_height=8,
|
||||
bar_width=2,
|
||||
bar_styles=["cyan", "magenta", "yellow", "green"],
|
||||
label_style="bold",
|
||||
)
|
||||
print(vertical)
|
||||
|
||||
print("\n[bold]Example 7: Grouped Horizontal Bars[/bold]")
|
||||
grouped_quarters = {
|
||||
"Q1": {"North": 32, "South": 28, "East": 25},
|
||||
"Q2": {"North": 38, "South": 30, "East": 29},
|
||||
"Q3": {"North": 34, "South": 35, "East": 30},
|
||||
"Q4": {"North": 40, "South": 37, "East": 33},
|
||||
}
|
||||
chart7 = BarChart(
|
||||
grouped_quarters,
|
||||
width=70,
|
||||
show_values=True,
|
||||
bar_width=2,
|
||||
group_styles=["red", "green", "cyan"],
|
||||
label_style="bold",
|
||||
value_style="dim",
|
||||
)
|
||||
print(chart7)
|
||||
|
||||
print("\n[bold]Example 8: Grouped Vertical Bars[/bold]")
|
||||
chart8 = BarChart(
|
||||
grouped_quarters,
|
||||
orientation="vertical",
|
||||
chart_height=12,
|
||||
bar_width=2,
|
||||
group_gap=2,
|
||||
group_styles=["red", "green", "cyan"],
|
||||
label_style="bold",
|
||||
)
|
||||
print(chart8)
|
||||
10
markup-before-fix.txt
Normal file
10
markup-before-fix.txt
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
ERROR: file or directory not found: tests/test_markup_validator.py
|
||||
|
||||
============================= test session starts =============================
|
||||
platform win32 -- Python 3.11.9, pytest-9.0.1, pluggy-1.6.0
|
||||
rootdir: C:\Users\BAE\rich-1
|
||||
configfile: pyproject.toml
|
||||
plugins: anyio-4.10.0
|
||||
collected 0 items
|
||||
|
||||
============================ no tests ran in 0.01s ============================
|
||||
0
rich-bar_output.txt
Normal file
0
rich-bar_output.txt
Normal file
452
rich/bar_chart.py
Normal file
452
rich/bar_chart.py
Normal file
|
|
@ -0,0 +1,452 @@
|
|||
"""Bar chart renderable for Rich."""
|
||||
|
||||
from collections.abc import Mapping, Sequence
|
||||
from typing import Dict, List, Optional, Tuple, Union
|
||||
|
||||
from .bar import END_BLOCK_ELEMENTS, FULL_BLOCK
|
||||
from .console import Console, ConsoleOptions, RenderResult
|
||||
from .jupyter import JupyterMixin
|
||||
from .measure import Measurement
|
||||
from .segment import Segment
|
||||
from .style import Style, StyleType
|
||||
|
||||
DEFAULT_COLORS = [
|
||||
"blue",
|
||||
"green",
|
||||
"yellow",
|
||||
"magenta",
|
||||
"cyan",
|
||||
"red",
|
||||
"bright_blue",
|
||||
"bright_green",
|
||||
]
|
||||
|
||||
|
||||
class BarChart(JupyterMixin):
|
||||
"""Render a flexible bar chart in the terminal.
|
||||
|
||||
Args:
|
||||
data (Union[Dict[str, float], List[Tuple[str, float]], Sequence[float]]):
|
||||
Data to display. Can be:
|
||||
- Dict mapping labels to values
|
||||
- Dict mapping labels to dicts of grouped values
|
||||
- List of (label, value) tuples
|
||||
- Sequence of values (labels will be auto-generated)
|
||||
width (int, optional): Width of the chart, or None for maximum width. Defaults to None.
|
||||
max_value (float, optional): Maximum value for scaling. If None, uses max value from data. Defaults to None.
|
||||
show_values (bool, optional): Show values on bars (horizontal only). Defaults to True.
|
||||
bar_width (int, optional): Width of each bar in characters. Defaults to 1.
|
||||
orientation (str, optional): Either ``"horizontal"`` or ``"vertical"``. Defaults to ``"horizontal"``.
|
||||
chart_height (int, optional): Height (in rows) of a vertical chart. Defaults to 10 if not given.
|
||||
group_labels (Sequence[str], optional): Optional ordering of grouped series labels.
|
||||
group_styles (Sequence[StyleType], optional): Styles for grouped series (cycled if fewer than groups).
|
||||
group_gap (int, optional): Number of spaces between grouped bars. Defaults to 1.
|
||||
style (StyleType, optional): Default style for bars. Defaults to None.
|
||||
bar_styles (Sequence[StyleType], optional): Styles for each single bar (cycled). Defaults to None.
|
||||
label_style (StyleType, optional): Style for labels. Defaults to None.
|
||||
value_style (StyleType, optional): Style for values. Defaults to None.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
data: Union["Dict[str, float]", List[Tuple[str, float]], Sequence[float]],
|
||||
*,
|
||||
width: Optional[int] = None,
|
||||
max_value: Optional[float] = None,
|
||||
show_values: bool = True,
|
||||
bar_width: int = 1,
|
||||
orientation: str = "horizontal",
|
||||
chart_height: Optional[int] = None,
|
||||
group_labels: Optional[Sequence[str]] = None,
|
||||
group_styles: Optional[Sequence[StyleType]] = None,
|
||||
group_gap: int = 1,
|
||||
style: Optional[StyleType] = None,
|
||||
bar_styles: Optional[Sequence[StyleType]] = None,
|
||||
label_style: Optional[StyleType] = None,
|
||||
value_style: Optional[StyleType] = None,
|
||||
):
|
||||
items = self._coerce_items(data)
|
||||
if not items:
|
||||
raise ValueError("Data cannot be empty")
|
||||
|
||||
self.width = width
|
||||
self.show_values = show_values
|
||||
self.bar_width = max(1, bar_width)
|
||||
self.orientation = orientation
|
||||
self.chart_height = chart_height
|
||||
self.style = style
|
||||
self.bar_styles = list(bar_styles) if bar_styles else None
|
||||
self.label_style = label_style
|
||||
self.value_style = value_style
|
||||
|
||||
self.group_gap = max(0, group_gap)
|
||||
self._explicit_group_labels = list(group_labels) if group_labels else None
|
||||
self.group_styles = list(group_styles) if group_styles else None
|
||||
|
||||
self.grouped = False
|
||||
self.group_labels: List[str] = []
|
||||
self.simple_rows: List[Tuple[str, float]] = []
|
||||
self.group_rows: List[Tuple[str, List[float]]] = []
|
||||
|
||||
self._normalize_items(items)
|
||||
self._set_max_value(max_value)
|
||||
|
||||
# --------------------------------------------------------------------- #
|
||||
# Initialization helpers
|
||||
# --------------------------------------------------------------------- #
|
||||
|
||||
def _coerce_items(
|
||||
self,
|
||||
data: Union["Dict[str, float]", List[Tuple[str, float]], Sequence[float]],
|
||||
) -> List[Tuple[Union[str, int], Union[float, Mapping[str, float]]]]:
|
||||
if isinstance(data, Mapping):
|
||||
return list(data.items())
|
||||
if isinstance(data, Sequence):
|
||||
if not data:
|
||||
return []
|
||||
first = data[0]
|
||||
if isinstance(first, tuple) and len(first) == 2:
|
||||
return list(data) # type: ignore[list-item]
|
||||
return [(index, value) for index, value in enumerate(data)]
|
||||
raise TypeError("BarChart data must be a mapping or sequence")
|
||||
|
||||
def _normalize_items(
|
||||
self,
|
||||
items: List[Tuple[Union[str, int], Union[float, Mapping[str, float]]]],
|
||||
) -> None:
|
||||
first_value = items[0][1]
|
||||
if isinstance(first_value, Mapping):
|
||||
self.grouped = True
|
||||
self._normalize_grouped_items(items) # type: ignore[arg-type]
|
||||
else:
|
||||
self.simple_rows = [
|
||||
(str(label), float(value)) for label, value in items # type: ignore[arg-type]
|
||||
]
|
||||
|
||||
def _normalize_grouped_items(
|
||||
self,
|
||||
items: List[Tuple[Union[str, int], Mapping[str, float]]],
|
||||
) -> None:
|
||||
groups: List[str] = []
|
||||
seen = set()
|
||||
if self._explicit_group_labels:
|
||||
groups.extend(str(name) for name in self._explicit_group_labels)
|
||||
seen.update(groups)
|
||||
|
||||
normalized_rows: List[Tuple[str, Dict[str, float]]] = []
|
||||
for label, mapping in items:
|
||||
label_str = str(label)
|
||||
row_dict: Dict[str, float] = {}
|
||||
for key, value in mapping.items():
|
||||
key_str = str(key)
|
||||
row_dict[key_str] = float(value)
|
||||
if not self._explicit_group_labels and key_str not in seen:
|
||||
groups.append(key_str)
|
||||
seen.add(key_str)
|
||||
normalized_rows.append((label_str, row_dict))
|
||||
|
||||
if not groups:
|
||||
raise ValueError("Grouped data requires at least one series")
|
||||
|
||||
self.group_labels = groups
|
||||
for label, mapping in normalized_rows:
|
||||
row_values = [mapping.get(group, 0.0) for group in self.group_labels]
|
||||
self.group_rows.append((label, row_values))
|
||||
|
||||
def _set_max_value(self, max_value: Optional[float]) -> None:
|
||||
values: List[float]
|
||||
if self.grouped:
|
||||
values = [
|
||||
value
|
||||
for _, row in self.group_rows
|
||||
for value in row
|
||||
]
|
||||
else:
|
||||
values = [value for _, value in self.simple_rows]
|
||||
|
||||
if not values:
|
||||
values = [0.0]
|
||||
|
||||
computed_max = max(values)
|
||||
if computed_max <= 0:
|
||||
computed_max = 1.0
|
||||
|
||||
self.max_value = max_value if max_value is not None else computed_max
|
||||
if self.max_value <= 0:
|
||||
self.max_value = 1.0
|
||||
|
||||
# --------------------------------------------------------------------- #
|
||||
# Rendering helpers
|
||||
# --------------------------------------------------------------------- #
|
||||
|
||||
def _get_label_style(self) -> Optional[Style]:
|
||||
return Style.parse(str(self.label_style)) if self.label_style else None
|
||||
|
||||
def _get_value_style(self) -> Optional[Style]:
|
||||
return Style.parse(str(self.value_style)) if self.value_style else None
|
||||
|
||||
def _get_bar_style(self, index: int) -> Style:
|
||||
if self.bar_styles:
|
||||
raw_style = self.bar_styles[index % len(self.bar_styles)]
|
||||
elif self.style:
|
||||
raw_style = self.style
|
||||
else:
|
||||
raw_style = DEFAULT_COLORS[index % len(DEFAULT_COLORS)]
|
||||
return Style.parse(str(raw_style))
|
||||
|
||||
def _get_group_style(self, index: int) -> Style:
|
||||
if self.group_styles:
|
||||
raw_style = self.group_styles[index % len(self.group_styles)]
|
||||
elif self.bar_styles:
|
||||
raw_style = self.bar_styles[index % len(self.bar_styles)]
|
||||
elif self.style:
|
||||
raw_style = self.style
|
||||
else:
|
||||
raw_style = DEFAULT_COLORS[index % len(DEFAULT_COLORS)]
|
||||
return Style.parse(str(raw_style))
|
||||
|
||||
def _bar_length(self, value: float, bar_area_width: int) -> int:
|
||||
if self.max_value <= 0:
|
||||
return 0
|
||||
return int((value / self.max_value) * bar_area_width)
|
||||
|
||||
def __rich_console__(
|
||||
self, console: Console, options: ConsoleOptions
|
||||
) -> RenderResult:
|
||||
if self.orientation == "vertical":
|
||||
yield from self._render_vertical(console, options)
|
||||
else:
|
||||
yield from self._render_horizontal(console, options)
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# Horizontal rendering
|
||||
# ------------------------------------------------------------------ #
|
||||
|
||||
def _render_horizontal(
|
||||
self, console: Console, options: ConsoleOptions
|
||||
) -> RenderResult:
|
||||
if self.grouped:
|
||||
yield from self._render_horizontal_grouped(console, options)
|
||||
return
|
||||
|
||||
available_width = min(
|
||||
self.width if self.width is not None else options.max_width,
|
||||
options.max_width,
|
||||
)
|
||||
|
||||
max_label_len = max(len(label) for label, _ in self.simple_rows)
|
||||
label_width = max_label_len + 2
|
||||
|
||||
bar_area_width = available_width - label_width
|
||||
if self.show_values:
|
||||
bar_area_width -= 12
|
||||
if bar_area_width < 1:
|
||||
bar_area_width = 1
|
||||
|
||||
label_style = self._get_label_style()
|
||||
value_style = self._get_value_style()
|
||||
|
||||
for idx, (label, value) in enumerate(self.simple_rows):
|
||||
bar_style = self._get_bar_style(idx)
|
||||
bar_length = self._bar_length(value, bar_area_width)
|
||||
bar_text = self._make_horizontal_bar(bar_length)
|
||||
value_text = ""
|
||||
if self.show_values:
|
||||
value_text = f" {value:.2f}"
|
||||
|
||||
label_padding = " " * (label_width - len(label) - 1)
|
||||
line_segments: List[Segment] = [
|
||||
Segment(label_padding),
|
||||
Segment(label, label_style),
|
||||
Segment(" "),
|
||||
Segment(bar_text, bar_style),
|
||||
]
|
||||
if value_text:
|
||||
line_segments.append(Segment(value_text, value_style))
|
||||
line_segments.append(Segment.line())
|
||||
yield from line_segments
|
||||
|
||||
def _render_horizontal_grouped(
|
||||
self, console: Console, options: ConsoleOptions
|
||||
) -> RenderResult:
|
||||
available_width = min(
|
||||
self.width if self.width is not None else options.max_width,
|
||||
options.max_width,
|
||||
)
|
||||
|
||||
max_category_len = max(len(label) for label, _ in self.group_rows)
|
||||
label_width = max_category_len + 1
|
||||
max_group_len = max(len(name) for name in self.group_labels)
|
||||
group_width = max_group_len + 1
|
||||
|
||||
bar_area_width = available_width - label_width - group_width - 2
|
||||
if self.show_values:
|
||||
bar_area_width -= 12
|
||||
if bar_area_width < 1:
|
||||
bar_area_width = 1
|
||||
|
||||
label_style = self._get_label_style()
|
||||
value_style = self._get_value_style()
|
||||
|
||||
for label, values in self.group_rows:
|
||||
for group_index, value in enumerate(values):
|
||||
bar_length = self._bar_length(value, bar_area_width)
|
||||
bar_style = self._get_group_style(group_index)
|
||||
|
||||
category_column = label if group_index == 0 else ""
|
||||
category_column = category_column.ljust(label_width)
|
||||
group_name = self.group_labels[group_index]
|
||||
group_column = group_name.ljust(group_width)
|
||||
|
||||
line_segments: List[Segment] = [
|
||||
Segment(category_column, label_style if category_column.strip() else None),
|
||||
Segment(" "),
|
||||
Segment(group_column, label_style),
|
||||
Segment(" "),
|
||||
Segment(self._make_horizontal_bar(bar_length), bar_style),
|
||||
]
|
||||
if self.show_values:
|
||||
line_segments.append(Segment(f" {value:.2f}", value_style))
|
||||
line_segments.append(Segment.line())
|
||||
yield from line_segments
|
||||
|
||||
# Blank line between categories for readability
|
||||
yield Segment.line()
|
||||
|
||||
yield from self._render_group_legend()
|
||||
|
||||
def _make_horizontal_bar(self, bar_length: int) -> str:
|
||||
full_blocks = bar_length // self.bar_width
|
||||
remainder = bar_length % self.bar_width
|
||||
bar_chars = FULL_BLOCK * full_blocks
|
||||
if remainder > 0 and remainder < len(END_BLOCK_ELEMENTS):
|
||||
bar_chars += END_BLOCK_ELEMENTS[remainder]
|
||||
return bar_chars or ""
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# Vertical rendering
|
||||
# ------------------------------------------------------------------ #
|
||||
|
||||
def _render_vertical(
|
||||
self, console: Console, options: ConsoleOptions
|
||||
) -> RenderResult:
|
||||
if self.grouped:
|
||||
yield from self._render_vertical_grouped(console, options)
|
||||
return
|
||||
|
||||
chart_height = self._get_chart_height(options)
|
||||
label_style = self._get_label_style()
|
||||
|
||||
bar_heights = [
|
||||
self._bar_length(value, chart_height) for _, value in self.simple_rows
|
||||
]
|
||||
bar_styles = [self._get_bar_style(idx) for idx in range(len(bar_heights))]
|
||||
|
||||
gap = 1
|
||||
for row in range(chart_height, 0, -1):
|
||||
segments: List[Segment] = []
|
||||
for idx, height in enumerate(bar_heights):
|
||||
if height >= row:
|
||||
segments.append(Segment(FULL_BLOCK * self.bar_width, bar_styles[idx]))
|
||||
else:
|
||||
segments.append(Segment(" " * self.bar_width))
|
||||
if idx != len(bar_heights) - 1:
|
||||
segments.append(Segment(" " * gap))
|
||||
segments.append(Segment.line())
|
||||
yield from segments
|
||||
|
||||
label_segments: List[Segment] = []
|
||||
for idx, (label, _value) in enumerate(self.simple_rows):
|
||||
label_str = self._fit_label(label)
|
||||
label_segments.append(Segment(label_str, label_style))
|
||||
if idx != len(self.simple_rows) - 1:
|
||||
label_segments.append(Segment(" " * gap))
|
||||
label_segments.append(Segment.line())
|
||||
yield from label_segments
|
||||
|
||||
def _render_vertical_grouped(
|
||||
self, console: Console, options: ConsoleOptions
|
||||
) -> RenderResult:
|
||||
chart_height = self._get_chart_height(options)
|
||||
label_style = self._get_label_style()
|
||||
|
||||
group_count = len(self.group_labels)
|
||||
group_gap = self.group_gap
|
||||
category_gap = group_gap + 1
|
||||
|
||||
heights: List[List[int]] = []
|
||||
for label, values in self.group_rows:
|
||||
heights.append(
|
||||
[self._bar_length(value, chart_height) for value in values]
|
||||
)
|
||||
|
||||
for row in range(chart_height, 0, -1):
|
||||
segments: List[Segment] = []
|
||||
for cat_idx, category_heights in enumerate(heights):
|
||||
for group_index, bar_height in enumerate(category_heights):
|
||||
if bar_height >= row:
|
||||
style = self._get_group_style(group_index)
|
||||
segments.append(Segment(FULL_BLOCK * self.bar_width, style))
|
||||
else:
|
||||
segments.append(Segment(" " * self.bar_width))
|
||||
if group_index != group_count - 1:
|
||||
segments.append(Segment(" " * group_gap))
|
||||
if cat_idx != len(heights) - 1:
|
||||
segments.append(Segment(" " * category_gap))
|
||||
segments.append(Segment.line())
|
||||
yield from segments
|
||||
|
||||
category_width = group_count * self.bar_width + (group_count - 1) * group_gap
|
||||
label_segments: List[Segment] = []
|
||||
for cat_idx, (label, _values) in enumerate(self.group_rows):
|
||||
label_text = self._center_text(label, category_width)
|
||||
label_segments.append(Segment(label_text, label_style))
|
||||
if cat_idx != len(self.group_rows) - 1:
|
||||
label_segments.append(Segment(" " * category_gap))
|
||||
label_segments.append(Segment.line())
|
||||
yield from label_segments
|
||||
|
||||
yield from self._render_group_legend()
|
||||
|
||||
def _render_group_legend(self) -> RenderResult:
|
||||
if not self.grouped or not self.group_labels:
|
||||
return
|
||||
segments: List[Segment] = []
|
||||
for idx, group in enumerate(self.group_labels):
|
||||
style = self._get_group_style(idx)
|
||||
segments.append(Segment(FULL_BLOCK * self.bar_width, style))
|
||||
segments.append(Segment(f" {group} "))
|
||||
segments.append(Segment.line())
|
||||
yield from segments
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# Utilities
|
||||
# ------------------------------------------------------------------ #
|
||||
|
||||
def _get_chart_height(self, options: ConsoleOptions) -> int:
|
||||
default_height = 10
|
||||
max_height = options.height if options.height is not None else options.size.height
|
||||
chart_height = self.chart_height or min(default_height, max_height)
|
||||
return max(1, chart_height)
|
||||
|
||||
def _fit_label(self, label: str) -> str:
|
||||
if len(label) > self.bar_width:
|
||||
return label[: self.bar_width]
|
||||
return label.ljust(self.bar_width)
|
||||
|
||||
def _center_text(self, text: str, width: int) -> str:
|
||||
if width <= len(text):
|
||||
return text[:width]
|
||||
padding = width - len(text)
|
||||
left = padding // 2
|
||||
right = padding - left
|
||||
return f"{' ' * left}{text}{' ' * right}"
|
||||
|
||||
def __rich_measure__(
|
||||
self, console: Console, options: ConsoleOptions
|
||||
) -> Measurement:
|
||||
if self.width is not None:
|
||||
return Measurement(self.width, self.width)
|
||||
return Measurement(20, options.max_width)
|
||||
|
||||
|
||||
Loading…
Add table
Add a link
Reference in a new issue