Skip to content

Commit 41adfd4

Browse files
authored
Additional refactoring and consolidation (#120)
* Additional fixes - Missed a duplicate escape code / color issue - Refactor duplicate format code into util function - Convert old .format() calls to f-strings to simplify * Refactor - move vertical to chart class * Update module tests/examplesa * More refactoring and consolidating Remove uv.lock from repo - helps version testing
1 parent 1bdbd78 commit 41adfd4

File tree

12 files changed

+364
-1346
lines changed

12 files changed

+364
-1346
lines changed

.gitignore

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,22 @@
1+
2+
# OS
3+
.DS_Store
4+
5+
# Python
16
*.pyc
27
venv/
3-
.vim/
4-
.vscode/
58
.coverage
69
htmlcov/
7-
dist/
8-
build/
10+
uv.lock
911
termgraph.egg-info/
10-
.coverage
11-
htmlcov/
1212
.python-version
13-
.vimrc
13+
.ropeproject/
1414

15+
# Editors
16+
.vim/
17+
.vimrc
18+
.vscode/
19+
.claude/
20+
.gemini/
1521
CLAUDE.md
22+

data/gener_rdat.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
date = BASE + offset
1414
value = random.randint(-500, 500)
1515

16-
f.write('{} {}\n'.format(date, int(value)))
16+
f.write(f'{date} {int(value)}\n')
1717
f.close()
1818

1919

termgraph/chart.py

Lines changed: 179 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,51 @@
33
from __future__ import annotations
44
import math
55
import sys
6-
from typing import Union
6+
from typing import Union, List, Tuple
7+
from itertools import zip_longest
78
import colorama
8-
from .constants import TICK, AVAILABLE_COLORS
9-
from .utils import cvt_to_readable, normalize, print_row_core
9+
from .constants import TICK, SM_TICK, AVAILABLE_COLORS
10+
from .utils import cvt_to_readable, print_row_core
1011
from .data import Data
1112
from .args import Args
1213

1314
colorama.init()
1415

1516

17+
def format_value(
18+
value: Union[int, float], format_str_arg, percentage_arg, suffix_arg
19+
) -> str:
20+
"""Format a value consistently across chart types."""
21+
# Handle type conversions and defaults
22+
if format_str_arg is None or not isinstance(format_str_arg, str):
23+
format_str = "{:<5.2f}"
24+
else:
25+
format_str = format_str_arg
26+
27+
if percentage_arg is None or not isinstance(percentage_arg, bool):
28+
percentage = False
29+
else:
30+
percentage = percentage_arg
31+
32+
if suffix_arg is None or not isinstance(suffix_arg, str):
33+
suffix = ""
34+
else:
35+
suffix = suffix_arg
36+
37+
formatted_val = format_str.format(value)
38+
39+
if percentage and "%" not in formatted_val:
40+
try:
41+
# Convert to percentage
42+
numeric_value = float(formatted_val)
43+
formatted_val = f"{numeric_value * 100:.0f}%"
44+
except ValueError:
45+
# If conversion fails, just add % suffix
46+
formatted_val += "%"
47+
48+
return f" {formatted_val}{suffix}"
49+
50+
1651
class Colors:
1752
"""Class representing available color values for graphs."""
1853

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

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

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

66-
if has_header_content:
67-
print("\n\n")
100+
print("\n")
101+
elif title is not None:
102+
print()
68103

69104
def _normalize(self) -> list[list[float]]:
70105
"""Normalize the data and return it."""
71106
width = self.args.get_arg("width")
72107
if not isinstance(width, int):
73108
width = 50 # Default width
74-
return normalize(self.data.data, width)
109+
return self.data.normalize(width)
75110

76111

77112
class HorizontalChart(Chart):
@@ -114,7 +149,7 @@ def print_row(
114149
num_blocks=int(num_blocks),
115150
val_min=float(val_min),
116151
color=color,
117-
zero_as_small_tick=bool(self.args.get_arg("label_before"))
152+
zero_as_small_tick=bool(self.args.get_arg("label_before")),
118153
)
119154

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

135170
super().__init__(data, args)
136171

172+
def _normalize(self) -> list[list[float]]:
173+
"""Normalize the data and return it."""
174+
if self.args.get_arg("different_scale"):
175+
# Normalization per category
176+
normal_data: List[List[float]] = [[] for _ in range(len(self.data.data))]
177+
width = self.args.get_arg("width")
178+
if not isinstance(width, int):
179+
width = 50 # Default width
180+
181+
if self.data.dims and len(self.data.dims) > 1:
182+
for i in range(self.data.dims[1]):
183+
cat_data = [[dat[i]] for dat in self.data.data]
184+
185+
# Create temporary Data object for category data
186+
from .data import Data
187+
temp_data = Data(cat_data, [f"cat_{j}" for j in range(len(cat_data))])
188+
normal_cat_data = temp_data.normalize(width)
189+
190+
for row_idx, norm_val in enumerate(normal_cat_data):
191+
normal_data[row_idx].append(norm_val[0])
192+
return normal_data
193+
else:
194+
return super()._normalize()
195+
137196
def draw(self) -> None:
138197
"""Draws the chart"""
139198
self._print_header()
140199

141200
colors = (
142201
self.args.get_arg("colors")
143202
if self.args.get_arg("colors") is not None
144-
else [None] * (self.data.dims[1] if self.data.dims and len(self.data.dims) > 1 else 1)
203+
else [None]
204+
* (self.data.dims[1] if self.data.dims and len(self.data.dims) > 1 else 1)
145205
)
146206

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

185245
else:
186-
val, deg = cvt_to_readable(values[j], self.args.get_arg("percentage"))
246+
val, deg = cvt_to_readable(
247+
values[j], self.args.get_arg("percentage")
248+
)
187249
format_str = self.args.get_arg("format")
188250
if isinstance(format_str, str):
189251
formatted_val = format_str.format(val)
190252
else:
191-
formatted_val = "{:<5.2f}".format(val) # Default format
253+
formatted_val = f"{val:<5.2f}" # Default format
192254
tail = fmt.format(
193255
formatted_val,
194256
deg,
@@ -239,7 +301,9 @@ def draw(self) -> None:
239301
if isinstance(colors_arg, list):
240302
colors = colors_arg
241303
else:
242-
colors = [None] * (self.data.dims[1] if self.data.dims and len(self.data.dims) > 1 else 1)
304+
colors = [None] * (
305+
self.data.dims[1] if self.data.dims and len(self.data.dims) > 1 else 1
306+
)
243307

244308
val_min = self.data.find_min()
245309
normal_data = self._normalize()
@@ -249,7 +313,7 @@ def draw(self) -> None:
249313
# Hide the labels.
250314
label = ""
251315
else:
252-
label = "{:<{x}}: ".format(self.data.labels[i], x=self.data.find_max_label_length())
316+
label = f"{self.data.labels[i]:<{self.data.find_max_label_length()}}: "
253317

254318
if self.args.get_arg("space_between") and i != 0:
255319
print()
@@ -267,31 +331,102 @@ def draw(self) -> None:
267331
color=colors[j] if j < len(colors) else None,
268332
zero_as_small_tick=False,
269333
)
270-
334+
271335
if self.args.get_arg("no_values"):
272336
# Hide the values.
273337
tail = ""
274338
else:
275-
format_str = self.args.get_arg("format")
276-
if isinstance(format_str, str):
277-
formatted_sum = format_str.format(sum(values))
278-
else:
279-
formatted_sum = "{:<5.2f}".format(sum(values))
280-
if self.args.get_arg("percentage"):
281-
if "%" not in formatted_sum:
282-
try:
283-
# Convert to percentage
284-
numeric_value = float(formatted_sum)
285-
formatted_sum = f"{numeric_value * 100:.0f}%"
286-
except ValueError:
287-
# If conversion fails, just add % suffix
288-
formatted_sum += "%"
289-
290-
tail = " {}{}".format(formatted_sum, self.args.get_arg("suffix"))
291-
339+
tail = format_value(
340+
sum(values),
341+
self.args.get_arg("format"),
342+
self.args.get_arg("percentage"),
343+
self.args.get_arg("suffix"),
344+
)
345+
292346
print(tail)
293347

294348

349+
class VerticalChart(Chart):
350+
"""Class representing a vertical chart"""
351+
352+
def __init__(self, data: Data, args: Args = Args()):
353+
"""Initialize the vertical chart"""
354+
super().__init__(data, args)
355+
self.value_list: list[str] = []
356+
self.zipped_list: list[tuple[str, ...]] = []
357+
self.vertical_list: list[str] = []
358+
self.maxi = 0
359+
360+
def _prepare_vertical(self, value: float, num_blocks: int):
361+
"""Prepare the vertical graph data."""
362+
self.value_list.append(str(value))
363+
364+
if self.maxi < num_blocks:
365+
self.maxi = num_blocks
366+
367+
if num_blocks > 0:
368+
self.vertical_list.append((TICK * num_blocks))
369+
else:
370+
self.vertical_list.append(SM_TICK)
371+
372+
def draw(self) -> None:
373+
"""Draws the vertical chart"""
374+
self._print_header()
375+
376+
colors = self.args.get_arg("colors")
377+
color = colors[0] if colors and isinstance(colors, list) else None
378+
379+
for i in range(len(self.data.labels)):
380+
values = self.data.data[i]
381+
num_blocks = self.normal_data[i]
382+
for j in range(len(values)):
383+
self._prepare_vertical(values[j], int(num_blocks[j]))
384+
385+
# Zip_longest method in order to turn them vertically.
386+
for row in zip_longest(*self.vertical_list, fillvalue=" "):
387+
self.zipped_list.append(row)
388+
389+
result_list: List[Tuple[str, ...]] = []
390+
391+
if self.zipped_list:
392+
counter = 0
393+
width = self.args.get_arg("width")
394+
if not isinstance(width, int):
395+
width = 50 # Default width
396+
397+
# Combined with the maxi variable, escapes the appending method at
398+
# the correct point or the default one (width).
399+
for row in reversed(self.zipped_list):
400+
result_list.append(row)
401+
counter += 1
402+
403+
if self.maxi == width:
404+
if counter == width:
405+
break
406+
else:
407+
if counter == self.maxi:
408+
break
409+
410+
if color:
411+
sys.stdout.write(f"\033[{color}m")
412+
413+
for row in result_list:
414+
print(*row)
415+
416+
sys.stdout.write("\033[0m")
417+
418+
if result_list and not self.args.get_arg("no_values"):
419+
print("-" * len(result_list[0]) * 2)
420+
print(" ".join(self.value_list))
421+
422+
if result_list and not self.args.get_arg("no_labels"):
423+
print("-" * len(result_list[0]) * 2)
424+
# Print Labels
425+
labels = self.data.labels
426+
if labels:
427+
print(" ".join(labels))
428+
429+
295430
class HistogramChart(Chart):
296431
"""Class representing a histogram chart"""
297432

@@ -357,7 +492,11 @@ def draw(self) -> None:
357492
width = width_arg
358493
else:
359494
width = 50 # default
360-
normal_counts = normalize(count_list, width)
495+
496+
# Create temporary Data object for count data
497+
from .data import Data
498+
temp_data = Data(count_list, [f"bin_{i}" for i in range(len(count_list))])
499+
normal_counts = temp_data.normalize(width)
361500

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

368507
if not self.args.get_arg("no_labels"):
369-
print(
370-
"{:{x}} – {:{x}}: ".format(start_border, end_border, x=max_len), end=""
371-
)
508+
print(f"{start_border:{max_len}}{end_border:{max_len}}: ", end="")
372509

373510
num_blocks = normal_counts[i]
374511

@@ -383,20 +520,10 @@ def draw(self) -> None:
383520
if self.args.get_arg("no_values"):
384521
tail = ""
385522
else:
386-
format_str = self.args.get_arg("format")
387-
if isinstance(format_str, str):
388-
formatted_val = format_str.format(count_list[i][0])
389-
else:
390-
formatted_val = "{:<5.2f}".format(count_list[i][0])
391-
if self.args.get_arg("percentage"):
392-
if "%" not in formatted_val:
393-
try:
394-
# Convert to percentage
395-
numeric_value = float(formatted_val)
396-
formatted_val = f"{numeric_value * 100:.0f}%"
397-
except ValueError:
398-
# If conversion fails, just add % suffix
399-
formatted_val += "%"
400-
401-
tail = " {}{}".format(formatted_val, self.args.get_arg("suffix"))
402-
print(tail)
523+
tail = format_value(
524+
count_list[i][0],
525+
self.args.get_arg("format"),
526+
self.args.get_arg("percentage"),
527+
self.args.get_arg("suffix"),
528+
)
529+
print(tail)

0 commit comments

Comments
 (0)