diff --git a/termgraph/chart.py b/termgraph/chart.py index ae19130..5adba1a 100644 --- a/termgraph/chart.py +++ b/termgraph/chart.py @@ -15,36 +15,29 @@ just_fix_windows_console() def format_value( - value: Union[int, float], format_str_arg, percentage_arg, suffix_arg + value: Union[int, float], args ) -> 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 + format_str = args.get_arg("format") + # Initial format formatted_val = format_str.format(value) - if percentage and "%" not in formatted_val: + if args.get_arg("no_values") is True: + return "" + elif args.get_arg("percentage") and "%" not in formatted_val: try: # Convert to percentage - numeric_value = float(formatted_val) - formatted_val = f"{numeric_value * 100:.0f}%" + numeric_value = float(value) + formatted_val = format_str.format(numeric_value * 100) + "%" except ValueError: # If conversion fails, just add % suffix formatted_val += "%" + elif args.get_arg("no_readable") is False: + val, deg = cvt_to_readable(value) + formatted_val = f"{format_str.format(val)}{deg}" + suffix = args.get_arg("suffix") return f" {formatted_val}{suffix}" @@ -75,6 +68,10 @@ def __init__(self, data: Data, args: Args): self.args = args self.normal_data = self._normalize() + # Get custom tick if set, otherwise use default TICK + custom_tick = self.args.get_arg("custom_tick") + self.tick = custom_tick if isinstance(custom_tick, str) and custom_tick else TICK + def draw(self) -> None: """Draw the chart with the given data""" @@ -83,9 +80,6 @@ def draw(self) -> None: def _print_header(self) -> None: title = self.args.get_arg("title") - custom_tick = self.args.get_arg("custom_tick") - tick = custom_tick if isinstance(custom_tick, str) and custom_tick else TICK - if title is not None: print("") print(f"# {title}\n") @@ -98,7 +92,7 @@ def _print_header(self) -> None: if colors is not None and isinstance(colors, list): sys.stdout.write(f"\033[{colors[i]}m") # Start to write colorized. - sys.stdout.write(f"{tick} {self.data.categories[i]} ") + sys.stdout.write(f"{self.tick} {self.data.categories[i]} ") if colors: sys.stdout.write("\033[0m") # Back to original. @@ -147,9 +141,6 @@ def print_row( if doprint: print(label, tail, " ", end="") - # Get custom tick if set, otherwise use default TICK - custom_tick = self.args.get_arg("custom_tick") - tick = custom_tick if isinstance(custom_tick, str) and custom_tick else TICK print_row_core( value=float(value), @@ -157,7 +148,7 @@ def print_row( val_min=float(val_min), color=color, zero_as_small_tick=bool(self.args.get_arg("label_before")), - tick=tick, + tick=self.tick, ) if doprint: @@ -250,29 +241,14 @@ def draw(self) -> None: len_label = len(label) label = " " * len_label - if self.args.get_arg("label_before"): - fmt = "{}{}{}" - - else: - fmt = " {}{}{}" - - if self.args.get_arg("no_values"): - tail = self.args.get_arg("suffix") + tail = format_value( + values[j], + self.args + ) - else: - 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 = f"{val:<5.2f}" # Default format - tail = fmt.format( - formatted_val, - deg, - self.args.get_arg("suffix"), - ) + if self.args.get_arg("label_before"): + # remove leading " " from format_value + tail = tail.lstrip() if colors and isinstance(colors, list) and j < len(colors): color = colors[j] @@ -325,10 +301,6 @@ def draw(self) -> None: val_min = self.data.find_min() normal_data = self._normalize() - # Get custom tick if set, otherwise use default TICK - custom_tick = self.args.get_arg("custom_tick") - tick = custom_tick if isinstance(custom_tick, str) and custom_tick else TICK - for i in range(len(self.data.labels)): if self.args.get_arg("no_labels"): # Hide the labels. @@ -351,18 +323,12 @@ def draw(self) -> None: val_min=val_min, color=colors[j] if j < len(colors) else None, zero_as_small_tick=False, - tick=tick, + tick=self.tick, ) - if self.args.get_arg("no_values"): - # Hide the values. - tail = "" - else: tail = format_value( sum(values), - self.args.get_arg("format"), - self.args.get_arg("percentage"), - self.args.get_arg("suffix"), + self.args ) print(tail) @@ -381,13 +347,13 @@ def __init__(self, data: Data, args: Args = Args()): def _prepare_vertical(self, value: float, num_blocks: int): """Prepare the vertical graph data.""" - self.value_list.append(str(value)) + self.value_list.append(format_value(value, self.args)) if self.maxi < num_blocks: self.maxi = num_blocks if num_blocks > 0: - self.vertical_list.append((TICK * num_blocks)) + self.vertical_list.append((self.tick * num_blocks)) else: self.vertical_list.append(SM_TICK) @@ -525,10 +491,6 @@ def draw(self) -> None: temp_data = Data(count_list, [f"bin_{i}" for i in range(len(count_list))]) normal_counts = temp_data.normalize(width) - # Get custom tick if set, otherwise use default TICK - custom_tick = self.args.get_arg("custom_tick") - tick = custom_tick if isinstance(custom_tick, str) and custom_tick else TICK - for i, (start_border, end_border) in enumerate(zip(borders[:-1], borders[1:])): if colors and colors[0]: color = colors[0] @@ -546,16 +508,11 @@ def draw(self) -> None: val_min=0, # Histogram always starts from 0 color=color, zero_as_small_tick=False, - tick=tick, + tick=self.tick, ) - if self.args.get_arg("no_values"): - tail = "" - else: - tail = format_value( - count_list[i][0], - self.args.get_arg("format"), - self.args.get_arg("percentage"), - self.args.get_arg("suffix"), - ) + tail = format_value( + count_list[i][0], + self.args + ) print(tail) diff --git a/termgraph/utils.py b/termgraph/utils.py index 600e0ab..56203b7 100644 --- a/termgraph/utils.py +++ b/termgraph/utils.py @@ -6,7 +6,7 @@ from .constants import UNITS, TICK, SM_TICK -def cvt_to_readable(num, percentage=False): +def cvt_to_readable(num): """Return the number in a human readable format. Examples: @@ -14,10 +14,6 @@ def cvt_to_readable(num, percentage=False): 12550 -> (12.55, 'K') 19561100 -> (19.561, 'M') """ - - if percentage: - return (num * 100, '%') - if num >= 1 or num <= -1: neg = num < 0 num = abs(num) diff --git a/tests/test_charts.py b/tests/test_charts.py index 9704623..d220999 100644 --- a/tests/test_charts.py +++ b/tests/test_charts.py @@ -1,25 +1,25 @@ from termgraph.data import Data from termgraph.args import Args -from termgraph.chart import BarChart, VerticalChart +from termgraph.chart import BarChart, StackedChart, VerticalChart, HistogramChart def test_barchart_draws_correctly(): labels = ["2007", "2008", "2009", "2010", "2011", "2012", "2014"] data_values = [[183.32], [231.23], [16.43], [50.21], [508.97], [212.05], [1.0]] - + data = Data(data_values, labels) args = Args(width=50, no_labels=False, suffix="", no_values=False) - + chart = BarChart(data, args) - + # Capture the output of the draw method import io from contextlib import redirect_stdout - + f = io.StringIO() with redirect_stdout(f): chart.draw() output = f.getvalue() - + # Assert that the output contains the expected elements assert "2007: " in output assert "183.32" in output @@ -53,19 +53,196 @@ def test_verticalchart_draws_correctly(): def test_custom_tick_appears_in_output(): labels = ["A", "B"] data_values = [[10], [20]] - data = Data(data_values, labels) args = Args(custom_tick="😀") - chart = BarChart(data, args) + def test(ChartType): + chart = ChartType(data, args) - import io - from contextlib import redirect_stdout + import io + from contextlib import redirect_stdout - f = io.StringIO() - with redirect_stdout(f): - chart.draw() - output = f.getvalue() + f = io.StringIO() + with redirect_stdout(f): + chart.draw() + output = f.getvalue() + + # Assert that the custom tick appears in the output + assert "😀" in output + + run_for_all_charts(test, with_histo=True) + +def run_for_all_charts(func, with_histo=False): + """ Runs a test function with all chart types, HistogramChart is optional as data format works differently there""" + charts = [BarChart, StackedChart, VerticalChart] + + if with_histo: + charts.append(HistogramChart) + + for chart in charts: + print(f"running {func.__name__} for {chart.__name__}") + func(chart) + +def test_format_default(): + labels = ["A", "B"] + data_values = [[10000], [20000]] + data = Data(data_values, labels) + args = Args() + + def test(ChartType): + chart = ChartType(data, args) + + import io + from contextlib import redirect_stdout + + f = io.StringIO() + with redirect_stdout(f): + chart.draw() + output = f.getvalue() + + # values are default formatted and converted to readable + assert "10.00K" in output + assert "20.00K" in output + + run_for_all_charts(test) + +def test_format_no_readable(): + labels = ["A", "B"] + data_values = [[10000], [20000]] + data = Data(data_values, labels) + args = Args(no_readable=True) + + def test(ChartType): + chart = ChartType(data, args) + + import io + from contextlib import redirect_stdout + + f = io.StringIO() + with redirect_stdout(f): + chart.draw() + output = f.getvalue() + + # values are not converted to readable format + assert "10000" in output + assert "20000" in output + assert "10.00K" not in output + assert "20.00K" not in output + + run_for_all_charts(test) + +def test_format_percentage(): + labels = ["A", "B"] + data_values = [[0.1], [0.275]] + data = Data(data_values, labels) + args = Args(percentage=True) + + def test(ChartType): + chart = ChartType(data, args) + + import io + from contextlib import redirect_stdout + + f = io.StringIO() + with redirect_stdout(f): + chart.draw() + output = f.getvalue() + + # values are converted to percentage + assert "10.00%" in output + assert "27.50%" in output + + run_for_all_charts(test) + +def test_format_custom_format(): + labels = ["A", "B"] + data_values = [[10.375], [400.00000]] + data = Data(data_values, labels) + args = Args(format="{:3.1f}") + + def test(ChartType): + chart = ChartType(data, args) + + import io + from contextlib import redirect_stdout + + f = io.StringIO() + with redirect_stdout(f): + chart.draw() + output = f.getvalue() + + # values are formatted with custom format + assert "10.4" in output + assert "400.0" in output + + run_for_all_charts(test) + +def test_format_no_values(): + labels = ["A", "B"] + data_values = [[10], [20]] + data = Data(data_values, labels) + args = Args(no_values=True) + + def test(ChartType): + chart = ChartType(data, args) + + import io + from contextlib import redirect_stdout + + f = io.StringIO() + with redirect_stdout(f): + chart.draw() + output = f.getvalue() + + # values are not printed at all + assert "10" not in output + assert "20" not in output + + run_for_all_charts(test) + +def test_format_suffix(): + labels = ["A", "B"] + data_values = [[10], [20]] + data = Data(data_values, labels) + args = Args(suffix="suf") + + def test(ChartType): + chart = ChartType(data, args) + + import io + from contextlib import redirect_stdout + + f = io.StringIO() + with redirect_stdout(f): + chart.draw() + output = f.getvalue() + + # values are suffixed + assert "10.00suf" in output + assert "20.00suf" in output + + run_for_all_charts(test) + +def test_no_values_with_suffix(): + labels = ["A", "B"] + data_values = [[10], [20]] + data = Data(data_values, labels) + args = Args(no_values=True, suffix="suf") + + def test(ChartType): + chart = ChartType(data, args) + + import io + from contextlib import redirect_stdout + + f = io.StringIO() + with redirect_stdout(f): + chart.draw() + output = f.getvalue() + + # suffix not printed with no_values + assert "suf" not in output + assert "10" not in output + assert "20" not in output - # Assert that the custom tick appears in the output - assert "😀" in output + run_for_all_charts(test) \ No newline at end of file