Skip to content

Instantly share code, notes, and snippets.

@rezemika
Created June 6, 2017 18:25
Show Gist options
  • Save rezemika/992822a528e9bb02ef98fb5f7c508469 to your computer and use it in GitHub Desktop.
Save rezemika/992822a528e9bb02ef98fb5f7c508469 to your computer and use it in GitHub Desktop.
String tables and horizontal histograms in Python3
#!/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