Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Plot table along chart using matplotlib

at the moment I've got this piece of code:

import pandas as pd
import matplotlib.pyplot as plt
import matplotlib as mpl
import numpy as np
from matplotlib.font_manager import FontProperties


data = np.random.uniform(0, 1, 80).reshape(20, 4)
final_data = [['%.3f' % j for j in i] for i in data]

mpl.style.use('seaborn')
mpl.rc('xtick', labelsize = 7)
mpl.rc('ytick', labelsize = 7)

fig = plt.figure()

fig.subplots_adjust(left=0.1, wspace=0.1)
plt.subplot2grid((1, 4), (0, 0), colspan=3)

table_subplot = plt.subplot2grid((1, 4), (0, 3))

table = plt.table(cellText=final_data, colLabels=['A', 'B', 'C', 'D'], loc='center', cellLoc='center', colColours=['#FFFFFF', '#F3CC32', '#2769BD', '#DC3735'])
table.auto_set_font_size(False)
table.set_fontsize(7)
table.auto_set_column_width((-1, 0, 1, 2, 3))

for (row, col), cell in table.get_celld().items():
    if (row == 0):
        cell.set_text_props(fontproperties=FontProperties(weight='bold', size=7))

plt.axis('off')
plt.show()

which produce this as an ouput:

enter image description here

I know that the plot is empty, but I'm planning to add some data there, so I need to keep this in mind! I want to add one more row to the table to have a header. This row should only be over the last three columns, something like this:

      +-----------------+
      |      Header     |
+-----+-----------------+
|  A  |  B  |  C  |  D  |
+-----+-----+-----+-----+
| ... | ... | ... | ... |
+-----+-----+-----+-----+
| ... | ... | ... | ... |
+-----+-----+-----+-----+

The width of the header row should match the sum of the width of the A, B and C columns. I've bene playing around but I can not manage to get it... Can anyone help me?

Thanks!

like image 539
Néstor Avatar asked Nov 20 '25 11:11

Néstor


1 Answers

Matplotlib tables do not have the concept of "colspan" or "rowspan", where cells are span multiple columns or rows. One could think that a cell can be made three times as wide as other cells. However, that would introduce an unwanted shift

enter image description here

An option you have is to set the visible_edges of additional cells you manually add via .add_cell to the top of the table.

The visible edges can be "T": top, "B" : bottom, "L" : left or "R" : right.
Then setting the text to the middle cell makes the whole thing look like a single cell.

import matplotlib.pyplot as plt

data = [[1,2,3,4],[6,5,4,3],[1,3,5,1]]

table = plt.table(cellText=data, colLabels=['A', 'B', 'C', 'D'], loc='center', 
                  cellLoc='center', colColours=['#FFFFFF', '#F3CC32', '#2769BD', '#DC3735'])
table.auto_set_font_size(False)
h = table.get_celld()[(0,0)].get_height()
w = table.get_celld()[(0,0)].get_width()

# Create an additional Header
header = [table.add_cell(-1,pos, w, h, loc="center", facecolor="none") for pos in [1,2,3]]
header[0].visible_edges = "TBL"
header[1].visible_edges = "TB"
header[2].visible_edges = "TBR"
header[1].get_text().set_text("Header Header Header Header")

plt.axis('off')
plt.show()

enter image description here


Appendix

The above does not allow to colorize the background of a table cell. For this the following workaround can be used:

import numpy as np
import matplotlib.pyplot as plt
import matplotlib.table
from matplotlib.collections import LineCollection
from matplotlib.path import Path

class MyCell(matplotlib.table.CustomCell):
    def __init__(self, *args, visible_edges, **kwargs):
        super().__init__(*args, visible_edges=visible_edges, **kwargs)
        seg = np.array([[0.0, 0.0], [1.0, 0.0], [1.0, 1.0],
                        [0.0, 1.0], [0.0, 0.0]]).reshape(-1, 1, 2)
        segments = np.concatenate([seg[:-1], seg[1:]], axis=1)
        self.edgelines = LineCollection(segments, edgecolor=kwargs.get("edgecolor"))
        self._text.set_zorder(2)
        self.set_zorder(1)

    def set_transform(self, trans):
        self.edgelines.set_transform(trans)
        super().set_transform(trans)

    def draw(self, renderer):
        c = self.get_edgecolor()
        self.set_edgecolor((1,1,1,0))
        super().draw(renderer)
        self.update_segments(c)
        self.edgelines.draw(renderer)
        self.set_edgecolor(c)

    def update_segments(self, color):
        x, y = self.get_xy()
        w, h = self.get_width(), self.get_height()
        seg = np.array([[x, y], [x+w, y], [x+w, y+h],
                        [x, y+h], [x, y]]).reshape(-1, 1, 2)
        segments = np.concatenate([seg[:-1], seg[1:]], axis=1)
        self.edgelines.set_segments(segments)
        self.edgelines.set_linewidth(self.get_linewidth())
        colors = [color if edge in self._visible_edges else (1,1,1,0)
                    for edge in self._edges]
        self.edgelines.set_edgecolor(colors)

    def get_path(self):
        codes = [Path.MOVETO] + [Path.LINETO]*3 + [Path.CLOSEPOLY]
        return Path(
            [[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0], [0.0, 0.0]],
            codes, readonly=True)



matplotlib.table.CustomCell = MyCell

data = [[1,2,3,4],[6,5,4,3],[1,3,5,1]]

table = plt.table(cellText=data, colLabels=['A', 'B', 'C', 'D'], loc='center', 
                  cellLoc='center', colColours=['#FFFFFF', '#F3CC32', '#2769BD', '#DC3735'])
table.auto_set_font_size(False)
h = table.get_celld()[(0,0)].get_height()
w = table.get_celld()[(0,0)].get_width()

# Create an additional Header
header = [table.add_cell(-1,pos, w, h, loc="center", facecolor="limegreen") for pos in [1,2,3]]
header[0].visible_edges = "TBL"
header[1].visible_edges = "TB"
header[2].visible_edges = "TBR"
header[1].get_text().set_text("Header")

plt.axis('off')
plt.show()

enter image description here

Note however, that this will fail for cases where the header text is longer than the cell, due to the drawing order.

like image 185
ImportanceOfBeingErnest Avatar answered Nov 22 '25 00:11

ImportanceOfBeingErnest



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!