Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 14 additions & 7 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,15 +1,22 @@

# OS
.DS_Store

# Python
*.pyc
venv/
.vim/
.vscode/
.coverage
htmlcov/
dist/
build/
uv.lock
termgraph.egg-info/
.coverage
htmlcov/
.python-version
.vimrc
.ropeproject/

# Editors
.vim/
.vimrc
.vscode/
.claude/
.gemini/
CLAUDE.md

2 changes: 1 addition & 1 deletion data/gener_rdat.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
date = BASE + offset
value = random.randint(-500, 500)

f.write('{} {}\n'.format(date, int(value)))
f.write(f'{date} {int(value)}\n')
f.close()


Expand Down
231 changes: 179 additions & 52 deletions termgraph/chart.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,51 @@
from __future__ import annotations
import math
import sys
from typing import Union
from typing import Union, List, Tuple
from itertools import zip_longest
import colorama
from .constants import TICK, AVAILABLE_COLORS
from .utils import cvt_to_readable, normalize, print_row_core
from .constants import TICK, SM_TICK, AVAILABLE_COLORS
from .utils import cvt_to_readable, print_row_core
from .data import Data
from .args import Args

colorama.init()


def format_value(
value: Union[int, float], format_str_arg, percentage_arg, suffix_arg
) -> str:
"""Format a value consistently across chart types."""
# Handle type conversions and defaults
if format_str_arg is None or not isinstance(format_str_arg, str):
format_str = "{:<5.2f}"
else:
format_str = format_str_arg

if percentage_arg is None or not isinstance(percentage_arg, bool):
percentage = False
else:
percentage = percentage_arg

if suffix_arg is None or not isinstance(suffix_arg, str):
suffix = ""
else:
suffix = suffix_arg

formatted_val = format_str.format(value)

if percentage and "%" not in formatted_val:
try:
# Convert to percentage
numeric_value = float(formatted_val)
formatted_val = f"{numeric_value * 100:.0f}%"
except ValueError:
# If conversion fails, just add % suffix
formatted_val += "%"

return f" {formatted_val}{suffix}"


class Colors:
"""Class representing available color values for graphs."""

Expand Down Expand Up @@ -47,7 +82,6 @@ def draw(self) -> None:

def _print_header(self) -> None:
title = self.args.get_arg("title")
has_header_content = title is not None or len(self.data.categories) > 0

if title is not None:
print(f"# {title}\n")
Expand All @@ -63,15 +97,16 @@ def _print_header(self) -> None:
if colors:
sys.stdout.write("\033[0m") # Back to original.

if has_header_content:
print("\n\n")
print("\n")
elif title is not None:
print()

def _normalize(self) -> list[list[float]]:
"""Normalize the data and return it."""
width = self.args.get_arg("width")
if not isinstance(width, int):
width = 50 # Default width
return normalize(self.data.data, width)
return self.data.normalize(width)


class HorizontalChart(Chart):
Expand Down Expand Up @@ -114,7 +149,7 @@ def print_row(
num_blocks=int(num_blocks),
val_min=float(val_min),
color=color,
zero_as_small_tick=bool(self.args.get_arg("label_before"))
zero_as_small_tick=bool(self.args.get_arg("label_before")),
)

if doprint:
Expand All @@ -134,14 +169,39 @@ def __init__(self, data: Data, args: Args = Args()):

super().__init__(data, args)

def _normalize(self) -> list[list[float]]:
"""Normalize the data and return it."""
if self.args.get_arg("different_scale"):
# Normalization per category
normal_data: List[List[float]] = [[] for _ in range(len(self.data.data))]
width = self.args.get_arg("width")
if not isinstance(width, int):
width = 50 # Default width

if self.data.dims and len(self.data.dims) > 1:
for i in range(self.data.dims[1]):
cat_data = [[dat[i]] for dat in self.data.data]

# Create temporary Data object for category data
from .data import Data
temp_data = Data(cat_data, [f"cat_{j}" for j in range(len(cat_data))])
normal_cat_data = temp_data.normalize(width)

for row_idx, norm_val in enumerate(normal_cat_data):
normal_data[row_idx].append(norm_val[0])
return normal_data
else:
return super()._normalize()

def draw(self) -> None:
"""Draws the chart"""
self._print_header()

colors = (
self.args.get_arg("colors")
if self.args.get_arg("colors") is not None
else [None] * (self.data.dims[1] if self.data.dims and len(self.data.dims) > 1 else 1)
else [None]
* (self.data.dims[1] if self.data.dims and len(self.data.dims) > 1 else 1)
)

val_min = self.data.find_min()
Expand Down Expand Up @@ -183,12 +243,14 @@ def draw(self) -> None:
tail = self.args.get_arg("suffix")

else:
val, deg = cvt_to_readable(values[j], self.args.get_arg("percentage"))
val, deg = cvt_to_readable(
values[j], self.args.get_arg("percentage")
)
format_str = self.args.get_arg("format")
if isinstance(format_str, str):
formatted_val = format_str.format(val)
else:
formatted_val = "{:<5.2f}".format(val) # Default format
formatted_val = f"{val:<5.2f}" # Default format
tail = fmt.format(
formatted_val,
deg,
Expand Down Expand Up @@ -239,7 +301,9 @@ def draw(self) -> None:
if isinstance(colors_arg, list):
colors = colors_arg
else:
colors = [None] * (self.data.dims[1] if self.data.dims and len(self.data.dims) > 1 else 1)
colors = [None] * (
self.data.dims[1] if self.data.dims and len(self.data.dims) > 1 else 1
)

val_min = self.data.find_min()
normal_data = self._normalize()
Expand All @@ -249,7 +313,7 @@ def draw(self) -> None:
# Hide the labels.
label = ""
else:
label = "{:<{x}}: ".format(self.data.labels[i], x=self.data.find_max_label_length())
label = f"{self.data.labels[i]:<{self.data.find_max_label_length()}}: "

if self.args.get_arg("space_between") and i != 0:
print()
Expand All @@ -267,31 +331,102 @@ def draw(self) -> None:
color=colors[j] if j < len(colors) else None,
zero_as_small_tick=False,
)

if self.args.get_arg("no_values"):
# Hide the values.
tail = ""
else:
format_str = self.args.get_arg("format")
if isinstance(format_str, str):
formatted_sum = format_str.format(sum(values))
else:
formatted_sum = "{:<5.2f}".format(sum(values))
if self.args.get_arg("percentage"):
if "%" not in formatted_sum:
try:
# Convert to percentage
numeric_value = float(formatted_sum)
formatted_sum = f"{numeric_value * 100:.0f}%"
except ValueError:
# If conversion fails, just add % suffix
formatted_sum += "%"

tail = " {}{}".format(formatted_sum, self.args.get_arg("suffix"))

tail = format_value(
sum(values),
self.args.get_arg("format"),
self.args.get_arg("percentage"),
self.args.get_arg("suffix"),
)

print(tail)


class VerticalChart(Chart):
"""Class representing a vertical chart"""

def __init__(self, data: Data, args: Args = Args()):
"""Initialize the vertical chart"""
super().__init__(data, args)
self.value_list: list[str] = []
self.zipped_list: list[tuple[str, ...]] = []
self.vertical_list: list[str] = []
self.maxi = 0

def _prepare_vertical(self, value: float, num_blocks: int):
"""Prepare the vertical graph data."""
self.value_list.append(str(value))

if self.maxi < num_blocks:
self.maxi = num_blocks

if num_blocks > 0:
self.vertical_list.append((TICK * num_blocks))
else:
self.vertical_list.append(SM_TICK)

def draw(self) -> None:
"""Draws the vertical chart"""
self._print_header()

colors = self.args.get_arg("colors")
color = colors[0] if colors and isinstance(colors, list) else None

for i in range(len(self.data.labels)):
values = self.data.data[i]
num_blocks = self.normal_data[i]
for j in range(len(values)):
self._prepare_vertical(values[j], int(num_blocks[j]))

# Zip_longest method in order to turn them vertically.
for row in zip_longest(*self.vertical_list, fillvalue=" "):
self.zipped_list.append(row)

result_list: List[Tuple[str, ...]] = []

if self.zipped_list:
counter = 0
width = self.args.get_arg("width")
if not isinstance(width, int):
width = 50 # Default width

# Combined with the maxi variable, escapes the appending method at
# the correct point or the default one (width).
for row in reversed(self.zipped_list):
result_list.append(row)
counter += 1

if self.maxi == width:
if counter == width:
break
else:
if counter == self.maxi:
break

if color:
sys.stdout.write(f"\033[{color}m")

for row in result_list:
print(*row)

sys.stdout.write("\033[0m")

if result_list and not self.args.get_arg("no_values"):
print("-" * len(result_list[0]) * 2)
print(" ".join(self.value_list))

if result_list and not self.args.get_arg("no_labels"):
print("-" * len(result_list[0]) * 2)
# Print Labels
labels = self.data.labels
if labels:
print(" ".join(labels))


class HistogramChart(Chart):
"""Class representing a histogram chart"""

Expand Down Expand Up @@ -357,7 +492,11 @@ def draw(self) -> None:
width = width_arg
else:
width = 50 # default
normal_counts = normalize(count_list, width)

# Create temporary Data object for count data
from .data import Data
temp_data = Data(count_list, [f"bin_{i}" for i in range(len(count_list))])
normal_counts = temp_data.normalize(width)

for i, (start_border, end_border) in enumerate(zip(borders[:-1], borders[1:])):
if colors and colors[0]:
Expand All @@ -366,9 +505,7 @@ def draw(self) -> None:
color = None

if not self.args.get_arg("no_labels"):
print(
"{:{x}} – {:{x}}: ".format(start_border, end_border, x=max_len), end=""
)
print(f"{start_border:{max_len}} – {end_border:{max_len}}: ", end="")

num_blocks = normal_counts[i]

Expand All @@ -383,20 +520,10 @@ def draw(self) -> None:
if self.args.get_arg("no_values"):
tail = ""
else:
format_str = self.args.get_arg("format")
if isinstance(format_str, str):
formatted_val = format_str.format(count_list[i][0])
else:
formatted_val = "{:<5.2f}".format(count_list[i][0])
if self.args.get_arg("percentage"):
if "%" not in formatted_val:
try:
# Convert to percentage
numeric_value = float(formatted_val)
formatted_val = f"{numeric_value * 100:.0f}%"
except ValueError:
# If conversion fails, just add % suffix
formatted_val += "%"

tail = " {}{}".format(formatted_val, self.args.get_arg("suffix"))
print(tail)
tail = format_value(
count_list[i][0],
self.args.get_arg("format"),
self.args.get_arg("percentage"),
self.args.get_arg("suffix"),
)
print(tail)
Loading