Reputation: 215
Is there a way to specify multiple headers with merged cells in python?
example dataset:
from tabulate import tabulate
cols = ["ID", "Config\nA", "Config\nB", "Config\nC", "Config\nD", "Oth"]
rows = [[ "0x0", "2", "0", "0", "4", "3"],
[ "0x1", "0", "0", "0", "0", "4"],
[ "0x2", "0", "2", "0", "1", "5"]]
print(tabulate(rows, headers=cols,tablefmt="pretty"))
current output from tabulate:
+-----+--------+--------+--------+--------+--------+
| ID | Config | Config | Config | Config | Oth |
| | A | B | C | D | |
+-----+--------+--------+--------+--------+--------+
| 0x0 | 2 | 0 | 0 | 4 | 3 |
| 0x1 | 0 | 0 | 0 | 0 | 4 |
| 0x2 | 0 | 1 | 0 | 1 | 5 |
+-----+--------+--------+--------+--------+--------+
desired output :
+-----+---+---+---+---+-----+
| ID | Config | Oth |
+ +---+---+---+---+ |
| | A | B | C | D | |
+-----+---+---+---+---+-----+
| 0x0 | 2 | 0 | 0 | 4 | 3 |
| 0x1 | 0 | 0 | 0 | 0 | 4 |
| 0x2 | 0 | 2 | 0 | 1 | 5 |
+-----+---+---+---+---+-----+
Upvotes: 10
Views: 9524
Reputation: 168
I'm not completely sure what you're going to use the table for, but I would perhaps suggest switching to the Pandas library. They have extensive support for all kinds of data labeling.
import pandas as pd
column_names = pd.DataFrame([["Config", "A"],
["Config", "B"],
["Config", "C"],
["Config", "D"],
["0th", ""]],
columns=["ID", ""])
rows = [["2", "0", "0", "4", "3"],
["0", "0", "0", "0", "4"],
["0", "2", "0", "1", "5"]]
columns = pd.MultiIndex.from_frame(column_names)
index = ["0x0", "0x1", "0x2"]
df = pd.DataFrame(rows, columns=columns, index=index)
display(df)
Upvotes: 6
Reputation: 163
I am not familiar with the tabulate module. However, I found a way to recreate this function and to include in its functionalities your wishes.
You could imitate the colspan and rowspan CSS/HTML properties. The tabulate function would accept two dictionaries as arguments. For example:
#headers
cols1 = ["ID", "Config", "x", "x", "x", "0th"] #x indicates an overridden cells
cols2 = ["x", "A", "B", "C", "D", "x"]
headers = [cols1, cols2]
#rows
rows = [
["0x0", "2", "0", "0", "4", "3"],
#etc.
]
#colspaning anf rowspaning
colspan = {(0, 1): 4} #colspan 4 for cell [0][1] in table (headers + rows)
rowspan = {(0, 0): 2, (0, 5): 2}
myTabulate(headers + rows, colspan, rowspan)
The expected output here should be similar to the one you are looking for.
Now, let's define the myTabulate
function. Let's first do it with one argument table
, the way you do already with the module. Then, I'll implement the colspan and rowspan arguments.
I don't really expect anyone to read it all, because it's quite long. What you can actually do is copy it and test it.
When calling the function, remember to only enter the first argument, headers + rows
.
#auxiliar functions
def writeCell(text, length):
extra_spaces = ""
for i in range(length - len(text) - 2):
extra_spaces += " " #according to column width
print(f"| {text} " + extra_spaces, end = "")
def getMaxColWidth(table, idx): #find the longest cell in the column to set the column's width
maxi = 0
for row in table:
if len(row) > idx: #avoid index out of range error
cur_len = len(row[idx]) + 2
if maxi < cur_len:
maxi = cur_len
return maxi
def getMaxRowLen(table): #find longest row list (in terms of elements)
maxi = 0
for row in table:
cur_len = len(row)
if maxi < cur_len:
maxi = cur_len
return maxi
def getAllColLen(table): #collect in a list the widths of each column
widths = [getMaxColWidth(table, i) for i in range(getMaxRowLen(table))]
return widths
def getMaxRowWidth(table): #set the width of the table
maxi = 0
for i in range(len(table)):
cur_len = sum(getAllColLen(table)) + len(getAllColLen(table)) + 1 # "|" at borders and between cells
if maxi < cur_len:
maxi = cur_len
return maxi
def drawBorder(table):
col_widths = getAllColLen(table)
length = getMaxRowWidth(table)
cell_w_count = 0
cell_counter = 0
for i in range(length):
if i == cell_w_count or i == length - 1:
print("+", end = "")
if cell_counter + 1 != getMaxRowLen(table):
cell_w_count += col_widths[cell_counter] + 1
cell_counter += 1
else:
print("-", end = "")
print("") #next line (end = "\n")
#main function
def myTabulate(table):
table_width = getMaxRowWidth(table)
col_widths = getAllColLen(table)
for row in table:
drawBorder(table)
for i, elem in enumerate(row):
writeCell(elem, col_widths[i])
print("|") #end table row
drawBorder(table) #close bottom of table
This is the output:
+-----+--------+-----+---+---+-----+
| ID | Config | x | x | x | 0th |
+-----+--------+-----+---+---+-----+
| x | A | B | C | D | x |
+-----+--------+-----+---+---+-----+
| 0x0 | 2 | 0 | 0 | 4 | 3 |
+-----+--------+-----+---+---+-----+
| 0x1 | 0 | 0 | 0 | 0 | 4 |
+-----+--------+-----+---+---+-----+
| 0x2 | 0 | 2 | 0 | 1 | 5 |
+-----+--------+-----+---+---+-----+
Next, I am implementing the colspan and rowspan functionalities.
Colspan consists in printing spaces instead of using writeCell
.
Rowspan consists in printing spaces instead of horizontal border line.
Again, the code is quite long.
#auxiliar functions
def isInRowspan(y, x, rowspan):
rowspan_value = 0
row_i = 0
for i in range(y):
if (i, x) in rowspan.keys():
rowspan_value = rowspan[(i, x)]
row_i = i
if rowspan_value - (y - row_i) > 0:
return True
else:
return False
def writeCell(table, y, x, length, rowspan = {}):
text = table[y][x]
extra_spaces = ""
if isInRowspan(y, x, rowspan):
text = "|"
for i in range(length): #according to column width
text += " "
print(text, end = "")
else:
for i in range(length - len(text) - 2):
extra_spaces += " " #according to column width
print(f"| {text} " + extra_spaces, end = "")
def writeColspanCell(length, colspan_value): #length argument refers to sum of column widths
text = ""
for i in range(length + colspan_value - 1):
text += " "
print(text, end = "")
def getMaxColWidth(table, idx): #find the longest cell in the column to set the column's width
maxi = 0
for row in table:
if len(row) > idx: #avoid index out of range error
cur_len = len(row[idx]) + 2
if maxi < cur_len:
maxi = cur_len
return maxi
def getMaxRowLen(table): #find longest row list (in terms of elements)
maxi = 0
for row in table:
cur_len = len(row)
if maxi < cur_len:
maxi = cur_len
return maxi
def getAllColLen(table): #collect in a list the widths of each column
widths = [getMaxColWidth(table, i) for i in range(getMaxRowLen(table))]
return widths
def getMaxRowWidth(table): #set the width of the table
maxi = 0
for i in range(len(table)):
cur_len = sum(getAllColLen(table)) + len(getAllColLen(table)) + 1 # "|" at borders and between cells
if maxi < cur_len:
maxi = cur_len
return maxi
def drawBorder(table, y, colspan = {}, rowspan = {}):
col_widths = getAllColLen(table)
length = getMaxRowWidth(table)
cell_w_count = 0
cell_counter = 0
for i in range(length):
if isInRowspan(y, cell_counter - 1, rowspan) and not (i == cell_w_count or i == length - 1):
print(" ", end = "")
elif i == cell_w_count or i == length - 1:
print("+", end = "")
if cell_counter != getMaxRowLen(table):
cell_w_count += col_widths[cell_counter] + 1
cell_counter += 1
else:
print("-", end = "")
print("") #next line (end = "\n")
#main function
def myTabulate(table, colspan = {}, rowspan = {}):
table_width = getMaxRowWidth(table)
col_widths = getAllColLen(table)
for y, row in enumerate(table):
drawBorder(table, y, colspan, rowspan)
x = 0
while x < len(row): #altered for loop
writeCell(table, y, x, col_widths[x], rowspan)
if (y, x) in colspan.keys():
colspan_value = colspan[(y, x)]
writeColspanCell(sum(col_widths[x+1:x+colspan_value]), colspan_value)
x += colspan_value - 1
x += 1
print("|") #end table row
drawBorder(table, getMaxRowLen(table) - 1) #close bottom of table
The output is:
+-----+--------+---+---+---+-----+
| ID | Config | 0th |
+ +--------+---+---+---+ +
| | A | B | C | D | |
+-----+--------+---+---+---+-----+
| 0x0 | 2 | 0 | 0 | 4 | 3 |
+-----+--------+---+---+---+-----+
| 0x1 | 0 | 0 | 0 | 0 | 4 |
+-----+--------+---+---+---+-----+
| 0x2 | 0 | 2 | 0 | 1 | 5 |
+-----+--------+---+---+---+-----+
If you play around with it, you'll see it's very flexible.
Input:
colspan = {(0, 1): 3, (1, 2): 2, (3, 0): 2, (3, 2): 2}
rowspan = {(0, 0): 4, (0, 5): 2, (0, 3): 3, (0, 2): 2}
Output:
+-----+--------+---+---+---+-----+
| ID | Config | x | 0th |
+ +--------+ + +---+ +
| | A | | D | |
+ +--------+---+ +---+-----+
| | 2 | 0 | | 4 | 3 |
+ +--------+---+---+---+-----+
| | 0 | 0 | 4 |
+-----+--------+---+---+---+-----+
| 0x2 | 0 | 2 | 0 | 1 | 5 |
+-----+--------+---+---+---+-----+
If you want to rowspan entire lines, it'll be shorter to use dictionary comprehension. Input:
colspan = {(2, 0): 6, (3, 0): 6}
rowspan = {(2, i):2 for i in range(6)}
Output:
+-----+--------+---+---+---+-----+
| ID | Config | x | x | x | 0th |
+-----+--------+---+---+---+-----+
| x | A | B | C | D | x |
+-----+--------+---+---+---+-----+
| 0x0 |
+ + + + + + +
| |
+-----+--------+---+---+---+-----+
| 0x2 | 0 | 2 | 0 | 1 | 5 |
+-----+--------+---+---+---+-----+
Upvotes: 3