kiki
kiki

Reputation: 215

Python tabulate: to have multiple header with merged cell

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

Answers (2)

Martin Gardfjell
Martin Gardfjell

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)

enter image description here

Upvotes: 6

Lolrenz Omega
Lolrenz Omega

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.

Colspan & Rowspan

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.

Without colspan and rowspan

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   |
+-----+--------+-----+---+---+-----+

With colspan and rowspan

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   |
+-----+--------+---+---+---+-----+

Voilà. Hope it's useful.

Upvotes: 3

Related Questions