Skip to content

Commit

Permalink
Rewrite ascii using rich
Browse files Browse the repository at this point in the history
  • Loading branch information
janpipek committed Jan 10, 2025
1 parent 7557f14 commit 1cfa752
Show file tree
Hide file tree
Showing 5 changed files with 105 additions and 80 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ Rationale (for both): physt is dumb, but precise.
- (optional) astropy - additional binning algorithms
- (optional) folium - map plotting
- (optional) vega3 - for vega in-line in IPython notebook (note that to generate vega JSON, this is not necessary)
- (optional) xtermcolor - for ASCII color maps
- (optional) rich - console output including plots
- (testing) pytest
- (docs) sphinx, sphinx_rtd_theme, ipython

Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ plotly = ["plotly"]
# vega3 = ["vega3"]
folium = ["folium"]
# root = ["uproot3"] # TODO: Update to uproot4
terminal = ["xtermcolor", "rich"]
terminal = ["rich"]
all = [
"physt[astropy,dev,dask,pandas,polars,xarray,matplotlib,plotly,folium,terminal]"
]
Expand Down
149 changes: 98 additions & 51 deletions src/physt/plotting/ascii.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,24 @@
"""
from __future__ import annotations

import numpy as np
import rich.console
import rich.color
from rich.style import Style
from rich.text import Text
import typing
from contextlib import suppress

from physt.plotting.common import get_value_format

if typing.TYPE_CHECKING:
from physt.types import Histogram1D, Histogram2D


types: typing.Tuple[str, ...] = ("hbar",)
types: typing.Tuple[str, ...] = ("hbar", "map")

dims = {
"hbar": [1],
"map": [2],
}


Expand All @@ -29,70 +36,110 @@ def hbar(h1: "Histogram1D", width: int = 80, show_values: bool = False) -> None:
print("#" * data[i])


with suppress(ImportError):
import xtermcolor
SUPPORTED_CMAPS = ("Greys", "Greys_r")
DEFAULT_CMAP = SUPPORTED_CMAPS[1]

SHADING_CHARS = " ░▒▓█"
"""Characters used for shading in the ASCII map."""

SUPPORTED_CMAPS = ("Greys", "Greys_r")
DEFAULT_CMAP = SUPPORTED_CMAPS[1]
FULL_SQUARE_CHAR = SHADING_CHARS[-1]

def map(h2: "Histogram2D", **kwargs) -> None:
"""Heat map.

Note: Available only if xtermcolor present.
"""
def map(
h2: "Histogram2D",
use_color: typing.Optional[bool] = None,
**kwargs) -> None:
"""Heat map."""

# Value format
val_format = kwargs.pop("value_format", ".2f")
if isinstance(val_format, str):
console = rich.console.Console()
color_system = console.color_system
if use_color is None:
use_color = bool(color_system)

def value_format(val):
return ("{0:" + val_format + "}").format(val)
# Value format
value_format = get_value_format(kwargs.pop("value_format", ".2f"))
cmap_data = _get_cmap_data(h2.frequencies, kwargs)

data = (h2.frequencies / h2.frequencies.max() * 255).astype(int)
if use_color:
def _render_cell(value: float) -> Text:
color = rich.color.Color.from_rgb(
int(255 * value), int(255 * value), int(255 * value)
)
return Text(FULL_SQUARE_CHAR, style=Style(color=color))

# Colour map
cmap = kwargs.pop("cmap", DEFAULT_CMAP)
if cmap == "Greys":
data = 255 - data
colorbar_range = range(h2.shape[1] + 1, -1, -1)
cmap_data = 1.0 - cmap_data
colorbar_range = np.arange(h2.shape[1] + 1, -1, -1) / h2.shape[1]
elif cmap == "Greys_r":
colorbar_range = range(h2.shape[1] + 2)
colorbar_range = np.arange(h2.shape[1] + 1) / h2.shape[1]
else:
raise ValueError(
f"Unsupported colormap: {cmap}, select from: {SUPPORTED_CMAPS}"
)
colors = (65536 + 256 + 1) * data

print(
(value_format(h2.get_bin_right_edges(0)[-1]) + " →").rjust(
h2.shape[1] + 2, " "
)
else:
def _render_cell(value: float) -> Text:
return Text(SHADING_CHARS[int(np.clip(value * (len(SHADING_CHARS)), 0, len(SHADING_CHARS) - 1))])

colorbar_range = np.arange(h2.shape[1] + 1) / h2.shape[1]

console.print(
(value_format(h2.get_bin_right_edges(0)[-1]) + " →").rjust(
h2.shape[1] + 2, " "
)
print("+" + "-" * h2.shape[1] + "+")
for i in range(h2.shape[0] - 1, -1, -1):
line_frags = [
xtermcolor.colorize("█", bg=0, rgb=colors[i, j])
for j in range(h2.shape[1])
]
line = "|" + "".join(line_frags) + "|"
if i == h2.shape[0] - 1:
line += value_format(h2.get_bin_right_edges(1)[-1]) + " ↑"
if i == 0:
line += value_format(h2.get_bin_left_edges(1)[0]) + " ↓"
print(line)
print("+" + "-" * h2.shape[1] + "+")
print("←", value_format(h2.get_bin_left_edges(0)[0]))
colorbar_frags = [
xtermcolor.colorize(
"█", bg=0, rgb=(65536 + 256 + 1) * int(j * 255 / (h2.shape[1] + 2))
)
for j in colorbar_range
)
console.print("+" + "-" * h2.shape[1] + "+")
for i in range(h2.shape[0] - 1, -1, -1):
line_frags = ["|"]
line_frags += [
_render_cell(cmap_data[i, j])
for j in range(h2.shape[1])
]
colorbar = "".join(colorbar_frags)
print()
print("↓", 0)
print(colorbar)
print(str(h2.frequencies.max()).rjust(h2.shape[1], " "), "↑")

types = types + ("map",)
dims["map"] = [2]
line_frags.append("|")
if i == h2.shape[0] - 1:
line_frags.append(value_format(h2.get_bin_right_edges(1)[-1]) + " ↑")
if i == 0:
line_frags.append(value_format(h2.get_bin_left_edges(1)[0]) + " ↓")
console.print(*line_frags, sep="")
console.print("+" + "-" * h2.shape[1] + "+")
console.print("←", value_format(h2.get_bin_left_edges(0)[0]))
colorbar_frags = [
_render_cell(j)
for j in colorbar_range
]
console.print("↓", 0, sep="")
console.print(*colorbar_frags, sep="")
console.print(str(h2.frequencies.max()).rjust(h2.shape[1], " "), "↑")


def _get_cmap_data(data, kwargs) -> np.ndarray:
"""Get normalized values to be used with a colormap.
Parameters
----------
data : array_like
cmap_min : Optional[float] or "min"
By default 0. If "min", minimum value of the data.
cmap_max : Optional[float]
By default, maximum value of the data
cmap_normalize : Optional[str]
Returns
-------
normalized_data : array_like
"""
norm = kwargs.pop("cmap_normalize", None)
if norm == "log":
cmap_max = np.log(kwargs.pop("cmap_max", data.max()))
cmap_min = np.log(kwargs.pop("cmap_min", data[data > 0].min()))
return (np.log(data) - cmap_min) / (cmap_max - cmap_min)
elif not norm:
cmap_max = kwargs.pop("cmap_max", data.max())
cmap_min = kwargs.pop("cmap_min", 0)
if cmap_min == "min":
cmap_min = data.min()
return (data - cmap_min) / (cmap_max - cmap_min)
else:
raise ValueError(f"Unsupported normalization: {norm}")
2 changes: 2 additions & 0 deletions src/physt/plotting/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -288,3 +288,5 @@ def __call__(self, h1: Histogram1D, min_: float, max_: float) -> TickCollection:
ticks = self.get_time_ticks(h1, level, min_, max_)
tick_labels = self.format_time_ticks(ticks, level=level)
return ticks, tick_labels


30 changes: 3 additions & 27 deletions uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 1cfa752

Please sign in to comment.