33from __future__ import annotations
44import math
55import sys
6- from typing import Union
6+ from typing import Union , List , Tuple
7+ from itertools import zip_longest
78import 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
1011from .data import Data
1112from .args import Args
1213
1314colorama .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+
1651class 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
77112class 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+
295430class 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