Editable table in Matplotlib: How to superimpose a TextBox widget on a table cell?

后端 未结 2 2033
北海茫月
北海茫月 2021-01-24 07:18

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

相关标签:
2条回答
  • 2021-01-24 08:00

    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.

    0 讨论(0)
  • 2021-01-24 08:00

    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()
    
    0 讨论(0)
提交回复
热议问题