Embed figure-level seaborn plot in nodes

I am trying to embed figure-level seaborn plot in the nodes. The widget creates its own figure, but FacetGrid also creates matplotlib.figure.Figure object Facetgrid.fig. How should I update the widget fig with the figure produced by FacetGrid?

What seaborn plot are you trying to embed?

I do have a couple Seaborn nodes in there. The way I did it was to use the ax keyword, which tells Seaborn which axes to use:

from nodeworks.tools.matplotlib_helper import Figure, FigureCanvas
from nodeworks.node import Node
import seaborn as sns

class MyNode(Node):
    def __init__(self):
        Node.__init__(self)
        self.fig = Figure((5.0, 4.0), dpi=100)
        self.canvas = FigureCanvas(self.fig)
        self.canvas.setParent(self.nodeGraphic.w)
        self.nodeGraphic.layout.insertWidget(2, self.canvas)

    def process(self):
        self.fig.clear()
        self.axes = self.fig.add_subplot(111)
        sns.heatmap(corr, ax=self.axes)

        # should use an even here to be thread safe
        self.canvas.draw()
        self.nodeGraphic.update()

However I see FacetGrid does not have an ax keyword: seaborn.FacetGrid — seaborn 0.11.1 documentation

Might have to dig into the source and find the axis objects and add them to the figure?

Matplotlib’s Figure does have an add_axes(ax) method: matplotlib.figure.Figure — Matplotlib 3.1.2 documentation

I see that Seaborn is creating it’s own figure in the FacetGrid:

I wonder if we could embed that figure?

This worked:

class TestNode(Node):
    name = 'FacetGrid Test'

    def __init__(self):
        Node.__init__(self)

        tips = sns.load_dataset("tips")
        g = sns.FacetGrid(tips, col="time",  row="smoker")
        g = g.map(plt.hist, "total_bill")
        self.canvas = FigureCanvas(g.fig)
        self.canvas.setParent(self.nodeGraphic.w)
        self.canvas.setMinimumSize(200, 100)

        self.nodeGraphic.layout.insertWidget(2, self.canvas)

        self.canvas.draw()
        self.nodeGraphic.update()

The way you did was to put all into initialization, but I want to put the sns.FacetGrid into process function which is called when the sheet is run.

class FacetGrid(Node):
    name = 'FacetGrid'

    def __init__(self):
        self.terminalOpts = OrderedDict([('data', {'widget': None,
                                                   'in': True,
                                                   'out': False,
                                                   'showlabel': True,
                                                   'dtype': pd.DataFrame,
                                                   'multiinput': False,
                                                   }),
                                         ])
        Node.__init__(self)

        self.nodeGraphic.layout.insertWidget(6, self.canvas)
        self.nodeGraphic.layout.insertWidget(7, self.mpl_toolbar)

 
    def process(self):
        tips = sns.load_dataset('tips')
        g = sns.FacetGrid(tips, col="time",  row="smoker")
        g = g.map(plt.hist, "total_bill")
        self.canvas = FigureCanvas(fig)
        self.canvas.setParent(self.nodeGraphic.w)
        self.canvas.setMinimumSize(200, 100)

        self.canvas.draw()
        self.nodeGraphic.update()

It hints:
Cannot set parent, new parent is in a different thread.
QObject::startTimer: Timers cannot be started from another thread
QBasicTimer::stop: Failed. Possibly trying to stop from a different thread

I have multithreading on by default, so when the sheet runs, the process method gets moved to a different thread. The consequence of this is that you can’t update any of the Qt objects (widgets etc.) from the process method directly. You have to use a signal/slot since the Qt signals are thread safe.

I got the following to work, seems really hackish to be creating/destroying the canvas all the time, but I could not find a way to just remove the figure and replace it with the new one created in the FacetGrid call. If FacetGrid had an option to provide the fig, this would be much easier. Actually you might be able to monkey patch the __init__ of FacetGrid :thinking:

class FacetGrid(Node):
    name = 'FacetGrid'
    updateGraphics = QtCore.Signal()

    def __init__(self):
        self.terminalOpts = {
            'data': {'in': True, 'dtype': pd.DataFrame, }
            }
        Node.__init__(self)
        self.updateGraphics.connect(self.refreshGraphics)
        self.g = None
        self.canvas = None

        # create a blank plot to make the node look ok
        self.create_canvas(Figure())

    def create_canvas(self, figure):
        ly = self.nodeGraphic.layout

        # remove the previous canvas
        if self.canvas is not None:
            ly.removeWidget(self.canvas)
            del self.canvas

        # create and add a new canvas
        self.canvas = FigureCanvas(figure)
        self.canvas.setParent(self.nodeGraphic.w)
        self.canvas.setMinimumSize(200, 100)
        ly.insertWidget(2, self.canvas)

    def process(self):
        # process data and create a new FacetGrid
        tips = sns.load_dataset("tips")
        self.g = sns.FacetGrid(tips, col="time",  row="smoker")
        self.g = self.g.map(plt.hist, "total_bill")

        # use event to update widgets, thread safe
        self.updateGraphics.emit()

    def refreshGraphics(self):
        if self.g is None:
            return

        # add the figure and draw
        self.create_canvas(self.g.fig)
        self.canvas.draw()
        self.nodeGraphic.update()