Created
June 6, 2017 18:25
-
-
Save rezemika/992822a528e9bb02ef98fb5f7c508469 to your computer and use it in GitHub Desktop.
String tables and horizontal histograms in Python3
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python3 | |
# -*- coding: utf-8 -*- | |
""" | |
Table and HorizontalHistogram are two simple Python classes. | |
The first make it easy to use a table, allowing, for example, | |
to vertically align several string elements. | |
The second allows to create an ASCII art horizontal histogram | |
adapted to the size of the current terminal. | |
It requires the first class to work properly. | |
Published under AGPLv3 licence by rezemika. | |
""" | |
import os | |
class Table(): | |
def __init__(self, rows=1, cols=1): | |
""" | |
Allows to create an ASCII art horizontal histogram | |
adapted to the size of the current terminal. | |
""" | |
self.table = None | |
self.table = [] | |
self.rows = rows | |
self.cols = cols | |
for row in range(self.rows): | |
self.table.append(self.cols*['']) | |
return | |
def get_term_size(self): | |
""" | |
Returns the size of the current terminal. | |
/!\ Uses the "stty size" command, so it works only on Unix systems. | |
""" | |
rows, columns = os.popen('stty size', 'r').read().split() | |
return int(rows), int(columns) | |
def set(self, row, col, value): | |
""" | |
Assigns a given value to a given position cell. | |
Returns False in case of IndexError, True else. | |
""" | |
try: | |
self.table[row][col] = value | |
return True | |
except IndexError: | |
return False | |
def append_col(self, after=True): | |
""" | |
Appends a column to the table. | |
Accepts an argument "after=False" to make it the first column. | |
""" | |
i = 0 | |
for row in range(self.rows): | |
current_row = self.table[i] | |
if not after: | |
self.table[i] = [''] + current_row | |
else: | |
self.table[i] = current_row + [''] | |
i += 1 | |
self.cols += 1 | |
return | |
def append_row(self, after=True): | |
""" | |
Appends a line to the table. | |
Accepts an argument "after=False" to make it the first line. | |
""" | |
if not after: | |
current_table = self.table | |
self.table = self.cols*[''] + current_table | |
else: | |
self.table.append(self.cols*['']) | |
return | |
def render(self): | |
""" | |
Returns a string of the current table. | |
The empty strings are rendered by spaces. | |
""" | |
output = '' | |
i = 0 | |
for row in range(self.rows+1): | |
row_content = '' | |
for col in range(self.cols): | |
try: | |
if self.table[i][col] != '': | |
row_content += self.table[i][col] | |
else: | |
row_content += ' ' | |
except IndexError: | |
continue | |
if col == self.cols - 1 and row != 0: | |
output += '\n' | |
output += ''.join(row_content) | |
i += 1 | |
if not output: | |
return '' | |
else: | |
return output | |
def __str__(self): | |
return self.render() | |
class HorizontalHistogram(): | |
def __init__(self, x_axis=True, x_axis_graduations=True, x_spacing=20, x_numbers=False, legend=True, force_size=False, bar_char='█'): | |
""" | |
Allows to create an ASCII art horizontal histogram | |
adapted to the size of the current terminal. | |
Optionnal arguments : | |
- x_axis=<[True]/False> : defines if the x axis should be displayed | |
- x_axis_graduations=<[True]/False> : defines if the X axis should be captioned | |
- x_spacing=<int> : defines the spacing (in %) between two graduations of the x-axis | |
- x_numbers=<True/[False]> : defined if the X axis should be captioned digitally | |
- legend=<[True]/False> : defines if the bar captions should be displayed | |
- force_size=<int> : force a width (in columns), instead of using the current terminal size | |
- bar_char=<str> : defines the character to be used for bars (█ by default) | |
""" | |
self.x_axis = x_axis | |
self.x_axis_graduations = x_axis_graduations | |
self.x_spacing = x_spacing | |
self.x_numbers = x_numbers | |
self.legend = legend | |
self.force_size = force_size | |
self.histogram = [] | |
self.bar_char = bar_char | |
self.right_offset = 1 | |
# Unchangeable yet. | |
self.left_offset = 4 | |
return | |
def append_bar(self, name, percentage, legend=None): | |
""" | |
Appends a bar to the histogram. | |
>> h.append_bar('A', 25, "Hundred divided by four") | |
Append a bar named "A" being 25% captioned | |
"Hundred divided by four". Caption is optional. | |
The name of a bar must be one character long. | |
""" | |
if percentage > 100: | |
raise ValueError("The value of the bar must be between 0 and 100.") | |
if len(name) != 1: | |
raise ValueError("The name of the bar must be one character long.") | |
self.histogram.append((name, percentage, legend)) | |
return | |
def bars_count(self): | |
""" | |
Returns the number of bars to display. | |
""" | |
return len(self.histogram) | |
def render(self): | |
""" | |
Renders the histogram and returns a string. | |
""" | |
# Calculates the starting index of the bars. | |
# Add "3" for the two spaces and the vertical bar. | |
self.left_offset = max([len(l[0]) for l in self.histogram])+3 | |
t = Table() | |
if self.force_size: | |
term_width = self.force_size | |
else: | |
term_width = t.get_term_size()[1] | |
n_rows = 2*self.bars_count()+1 | |
t = Table(rows=n_rows, cols=term_width-self.right_offset) | |
max_bar_size = term_width - self.left_offset | |
# Add the vertical bar. | |
left_bar_height = t.rows | |
if self.x_axis: | |
left_bar_height -= 1 | |
t.set(left_bar_height, self.left_offset-1, '+') | |
for i in range(self.left_offset, self.left_offset+4): | |
t.set(left_bar_height, i, '―') | |
# Calculation of the interval between two graduations. | |
x_axis_interval = int((self.x_spacing/100)*max_bar_size) | |
for i in range(x_axis_interval, max_bar_size, x_axis_interval): | |
t.set(left_bar_height, i+(self.left_offset-1), 'ˈ') | |
for row in range(left_bar_height): | |
t.set(row, self.left_offset-1, '|') | |
# Add the bars. | |
i = 1 | |
for bar in self.histogram: | |
# Round to the lower integer. | |
bar_size = int((bar[1]/100) * max_bar_size) | |
# Add the name of the bar. | |
t.set(i, 1, bar[0]) | |
for j in range(bar_size): | |
t.set(i, j+4, self.bar_char) | |
i += 2 | |
output = '\n' | |
output += t.render() | |
# Add the captions of the horizontal axis if needed. | |
if self.x_numbers: | |
graduations_interval = int((self.x_spacing/100)*max_bar_size) | |
graduations = (self.left_offset+len(str(self.x_spacing))-1)*' ' | |
j = self.x_spacing | |
for i in range(x_axis_interval, max_bar_size, graduations_interval): | |
graduations += (graduations_interval - len(str(j)))*' ' + str(j) | |
j += self.x_spacing | |
output += graduations | |
if not self.legend: | |
return output | |
else: | |
output = output + '\n' | |
for bar in self.histogram: | |
output += '\n' | |
if bar[2] is not None: | |
output += " {} ({}%) : {}".format(bar[0], bar[1], bar[2]) | |
else: | |
output += " {} ({}%)".format(bar[0], bar[1]) | |
output += '\n' | |
return output | |
def __str__(self): | |
return self.render() | |
if __name__ == '__main__': | |
h = HorizontalHistogram(x_numbers=True, x_axis_graduations=True) | |
h.append_bar('A', 20, "Hello world !") | |
h.append_bar('B', 50, "A 50% bar !") | |
h.append_bar('C', 95) | |
print(h.render()) | |
t = Table(2, 2) | |
t.set(0, 0, 'X') | |
t.set(0, 1, 'X') | |
t.set(1, 0, 'O') | |
t.set(1, 1, 'O') | |
t.append_col() | |
t.set(0, 2, 'O') | |
t.append_row() | |
t.set(2, 0, 'X') | |
t.set(2, 2, 'O') | |
print("-----\n") | |
print("A Tic-tac-toe game !\n") | |
print(t.render()) | |
exit(0) | |
""" | |
Output : | |
| | |
A |████████████████ | |
| | |
B |████████████████████████████████████████ | |
| | |
C |████████████████████████████████████████████████████████████████████████████ | |
+―――― ˈ ˈ ˈ ˈ ˈ | |
20 40 60 80 100 | |
A (20%) : Hello world ! | |
B (50%) : A 50% bar ! | |
C (95%) | |
----- | |
A Tic-tac-toe game ! | |
XXO | |
OO | |
X O | |
""" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment