I\'m progressing towards creating an interactive table in Matplotlib. I want the user to be able to click on a data cell in the table so they can edit its value. Based on the ad
The cell's position is indeed given in axes coordinates, while the TextBox
's axes lives in figure coordinates. You may transform in between the two coordinate systems as
trans = figure.transFigure.inverted()
trans2 = ax.transAxes
bbox = cell.get_bbox().transformed(trans2 + trans)
text_box_axes.set_position(bbox.bounds)
Of course you then also need to make sure the cell text is updated according to the content of the TextBox
, each time it is submitted.
The following would be a fully functional editable matplotlib table.
import matplotlib.pyplot as plt
from matplotlib.table import CustomCell
from matplotlib.widgets import TextBox
class EditableTable():
def __init__(self, table):
self.table = table
self.ax = self.table.axes
celld = table.get_celld()
for key in celld.keys():
if key[0] > 0 and key[1] > -1:
cell = celld[key]
cell.set_picker(True)
self.canvas = self.table.get_figure().canvas
self.cid = self.canvas.mpl_connect('pick_event', self.on_pick)
self.tba = self.ax.figure.add_axes([0,0,.01,.01])
self.tba.set_visible(False)
self.tb = TextBox(self.tba, '', initial="")
self.cid2 = self.tb.on_submit(self.on_submit)
self.currentcell = celld[(1,0)]
def on_pick(self, event):
if isinstance(event.artist, CustomCell):
# clear axes and delete textbox
self.tba.clear()
del self.tb
# make textbox axes visible
self.tba.set_visible(True)
self.currentcell = event.artist
# set position of textbox axes to the position of the current cell
trans = self.ax.figure.transFigure.inverted()
trans2 = self.ax.transAxes
bbox = self.currentcell.get_bbox().transformed(trans2 + trans)
self.tba.set_position(bbox.bounds)
# create new Textbox with text of the current cell
cell_text = self.currentcell.get_text().get_text()
self.tb = TextBox(self.tba, '', initial=cell_text)
self.cid2 = self.tb.on_submit(self.on_submit)
self.canvas.draw()
def on_submit(self, text):
# write the text box' text back to the current cell
self.currentcell.get_text().set_text(text)
self.tba.set_visible(False)
self.canvas.draw_idle()
column_labels = ('Length', 'Width', 'Height', 'Sold?')
row_labels = ['Ferrari', 'Porsche']
data = [[2.2, 1.6, 1.2, True],
[2.1, 1.5, 1.4, False]]
fig, ax = plt.subplots()
table = ax.table(cellText=data, colLabels=column_labels, rowLabels=row_labels,
cellLoc='center', loc='bottom')
et = EditableTable(table)
ax.axis('off')
plt.show()
Note however that there is some bug sometimes preventing the cell to be correctly updated. I haven't found out the reason for this yet.
Note that in a previous version of this, a single TextBox instance was used. However this led to untraceable errors. Instead one would need to create a new instance each time a cell is clicked, as in the above updated version.
Using @ImportanceOfBeingErnest's very helpful answer I was able to adapt my original code to a working solution. Yes, I know it uses horrible globals, etc but at least it works!
import matplotlib.pyplot as plt
from matplotlib.table import CustomCell
from matplotlib.widgets import TextBox
def on_pick(event):
if isinstance(event.artist, CustomCell):
global text_box, current_cell, table
if text_box is not None:
plt.gcf().delaxes(text_box.ax)
current_cell = event.artist
table_axes = table.axes
axes_to_display = table_axes.transAxes
display_to_figure = table_axes.figure.transFigure.inverted()
bbox = current_cell.get_bbox().transformed(axes_to_display + display_to_figure)
text_box_axes = plt.axes(bbox.bounds)
cell_text = current_cell.get_text().get_text()
text_box = TextBox(text_box_axes, '', initial=cell_text)
text_box.on_submit(update_table_cell)
plt.draw()
def update_table_cell(new_value):
global text_box, current_cell
# Get rid of the textbox:
plt.gcf().delaxes(text_box.ax)
current_cell.get_text().set_text(new_value)
text_box = None
current_cell = None
# TODO: Update the table data...
plt.draw()
column_labels = ('Length', 'Width', 'Height', 'Sold?')
row_labels = ['Ferrari', 'Porsche']
data = [[2.2, 1.6, 1.2, True],
[2.1, 1.5, 1.4, False]]
table = plt.table(cellText=data, colLabels=column_labels, rowLabels=row_labels, cellLoc='center', loc='bottom')
text_box = None
current_cell = None
celld = table.get_celld()
for key in celld.keys():
# Each key is a tuple of the form (row, column).
# Column headings are in row 0. Row headings are in column -1.
# So the first item of data in the table is actually at (1, 0).
if key[0] > 0 and key[1] > -1:
cell = celld[key]
cell.set_picker(True)
canvas = plt.gcf().canvas
canvas.mpl_connect('pick_event', on_pick)
plt.axis('off')
plt.show()