Matplotlib中的可编辑表:如何在表格单元格上叠加TextBox小部件?

时间:2022-09-12 08:52:08

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 advice of @ImportanceOfBeingErnest here I've registered a pick event handler for each cell of real data in the table. I can then detect which cell the user has clicked on. But I can't superimpose a TextBox object exactly over the picked cell so that to the user it looks like they're editing the cell they picked.

我正朝着在Matplotlib中创建交互式表的方向前进。我希望用户能够单击表中的数据单元格,以便他们可以编辑其值。根据@ImportanceOfBeingErnest的建议,我已经为表格中的每个真实数据单元注册了一个选择事件处理程序。然后我可以检测用户点击了哪个单元格。但我无法在拾取的单元格上精确叠加TextBox对象,因此对于用户来说,他们看起来正在编辑他们选择的单元格。

Dummy code to illustrate the issue:

用于说明问题的虚拟代码:

import matplotlib.pyplot as plt

from matplotlib.table import CustomCell
from matplotlib.widgets import TextBox

def on_pick(event):

    if isinstance(event.artist, CustomCell):
        cell = event.artist
        # Doesn't work because cell.get_y() is negative:
        #text_box_axes = plt.axes([cell.get_x(), cell.get_y(), cell.get_width(), cell.get_height()])

        # This doesn't work either but at least you can see the TextBox on the figure!
        text_box_axes = plt.axes([cell.get_x(), -cell.get_y(), cell.get_width(), cell.get_height()])

        cell_text = cell.get_text().get_text()
        TextBox(text_box_axes, '', initial=cell_text)
        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

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()

But if I run this and then click on, say, the cell with 1.2 in it I see this:

但是,如果我运行它,然后点击,比如说,其中有1.2的单元格,我看到了这个:

Matplotlib中的可编辑表:如何在表格单元格上叠加TextBox小部件?

So how do I get the bounds of the TextBox to exactly match the bounds of the cell that the user has clicked on?

那么如何让TextBox的边界与用户点击的单元格边界完全匹配?

It seems the axes for the textbox are relative to the whole figure rather than to the table itself.

看起来文本框的轴相对于整个图而不是表本身。

2 个解决方案

#1


0  

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

单元格的位置确实在轴坐标中给出,而TextBox的轴在图形坐标中。您可以在两个坐标系之间进行转换

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.

当然,您还需要确保每次提交时都根据TextBox的内容更新单元格文本。

The following would be a fully functional editable matplotlib table.

以下是一个功能齐全的可编辑matplotlib表。

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.

但请注意,有时会出现一些错误,无法正确更新单元格。我还没有找到原因。请注意,在此版本的早期版本中,使用了单个T​​extBox实例。然而,这导致了无法追踪的错误。相反,每次单击一个单元格时都需要创建一个新实例,如上面更新的版本所示。

#2


0  

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!

使用@ ImportanceOfBeingErnest非常有用的答案,我能够将原始代码调整为可行的解决方案。是的,我知道它使用可怕的全局等,但至少它有效!

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()

#1


0  

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

单元格的位置确实在轴坐标中给出,而TextBox的轴在图形坐标中。您可以在两个坐标系之间进行转换

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.

当然,您还需要确保每次提交时都根据TextBox的内容更新单元格文本。

The following would be a fully functional editable matplotlib table.

以下是一个功能齐全的可编辑matplotlib表。

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.

但请注意,有时会出现一些错误,无法正确更新单元格。我还没有找到原因。请注意,在此版本的早期版本中,使用了单个T​​extBox实例。然而,这导致了无法追踪的错误。相反,每次单击一个单元格时都需要创建一个新实例,如上面更新的版本所示。

#2


0  

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!

使用@ ImportanceOfBeingErnest非常有用的答案,我能够将原始代码调整为可行的解决方案。是的,我知道它使用可怕的全局等,但至少它有效!

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()