2

I am using matplotlib to interactively plot some patches and points.

I receive the data from a separate process via a queue and send them to my plot-process. That part of the code works fine and points are shown on the graph and continuously updated in the plot as expected.

Upon request from the user I would like to remove all the old patches in the plot and replace with new ones.

I thought it would be enough to perform:

# remove the old patch
patch.remove() 
# I also tried ax.cla() without any success
# create a new patch
monitor_box = path.Path([(305, 500), (11, -213), (300, -220), (500, 1734)])
patch = patches.PathPatch(monitor_box, facecolor='black', lw=1)
# add the patch to the axis
ax.add_patch(patch)

and then during the next iteration, the plot should be updated with new patch:

canvas.draw()

but when I use the above code, the patch still remains in the window and nothing changes. (I am still getting the points in the plot so that is at-least still being continuously updated)

Below I have provided a minimal working example of the issue. When you run the code, you can see that different points get plotted but the patches are never removed.

import matplotlib
matplotlib.use('TkAgg')
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg, NavigationToolbar2TkAgg
import multiprocessing
from Tkinter import *
import matplotlib.path as path
import matplotlib.patches as patches
import sys, thread, time

from random import randint

#Create a window
window=Tk()

sendProximityInfo = True
latest_published_msg = ""

def erasePatchesAndCreateNew_A():
    print "erasePatchesAndCreateNew_A"
    global line, ax, canvas
    global monitor_box
    global patch
    patch.remove()
    ax.cla()            
    monitor_box = path.Path([(35, 1677), (11, -213), (652, -220), (500, 1734)])
    patch = patches.PathPatch(monitor_box, facecolor='black', lw=1)

    ax.add_patch(patch)

def erasePatchesAndCreateNew_B():
    print "erasePatchesAndCreateNew_B"
    global line, ax, canvas
    global monitor_box
    global patch
    patch.remove()
    ax.cla()
    monitor_box = path.Path([(35, 500), (11, -213), (300, -220), (500, 1734)])
    patch = patches.PathPatch(monitor_box, facecolor='red', lw=1)

    ax.add_patch(patch)

monitor_box = path.Path([(35, 1677), (111, -213), (62, -220), (800, 1734)])
fig = matplotlib.figure.Figure()
ax  = fig.add_subplot(1,1,1)
ax.set_xlim(-1500,2000)
ax.set_ylim(-1500,2000)
patch = patches.PathPatch(monitor_box, facecolor='black', lw=1)
ax.add_patch(patch)

def main():
    erasePatchesAndCreateNew_B()

    #Create a queue to share data between process
    q = multiprocessing.Queue()
    #Create and start the simulation process
    simulate = multiprocessing.Process(None, simulation,args=(q,))
    simulate.start()   
    #Create the base plot
    plot()
    #Call a function to update the plot when there is new data
    updateplot(q)

    window.mainloop()
    print 'Done'
    simulate.join() # wait for the other process to finish as well

def plot():    #Function to create the base plot, make sure to make global the lines, axes, canvas and any part that you would want to update later
    global line, ax, canvas
    global monitor_box
    global patch

    fig = matplotlib.figure.Figure()
    ax  = fig.add_subplot(1,1,1)
    ax.set_xlim(-1500,2000)
    ax.set_ylim(-1500,2000)

    patch = patches.PathPatch(monitor_box, facecolor='black', lw=1)
    ax.add_patch(patch)

    ax.invert_yaxis()
    canvas = FigureCanvasTkAgg(fig, master=window)
    canvas.show()
    canvas.get_tk_widget().pack(side=TOP, fill=BOTH, expand=1)
    canvas._tkcanvas.pack(side=TOP, fill=BOTH, expand=1)
    line, = ax.plot([], [], 'ro')

def updateplot(q):
    try:       #Try to check if there is data in the queue
        result = q.get_nowait()

        if result != 'Q':
            x, y = result
            line.set_data(x, y)
            ax.draw_artist(line)
            canvas.draw()
            window.after(1,updateplot,q)
        else:
            print 'done'
    except:
        window.after(1,updateplot,q)

def simulation(q):   
    try:
        while True:
            for i in range(10):
                q.put( (randint(0,1500), randint(0,1500)) )
                time.sleep(1)
            erasePatchesAndCreateNew_A()
            time.sleep(1)
            for i in range(10):
                q.put( (randint(0,1500), randint(0,1500)) )
                time.sleep(1)
            erasePatchesAndCreateNew_B()
            time.sleep(1)
    except KeyboardInterrupt:
        print "received KeyboardInterrupt"
    finally:
        print "simulation ended"
        sys.exit()

if __name__ == '__main__':
    main()

Below is a screenshot from the program, the red dot moves around in the graph but the patch (black shape) never changes. enter image description here

theAlse
  • 5,577
  • 11
  • 68
  • 110
  • There is no call to `patch.remove()` within the `updateplot` function. So what exactly are you expecting to happen? – ImportanceOfBeingErnest Jan 13 '17 at 08:52
  • @ImportanceOfBeingErnest I just did not add that part of the code, configureForPortrait() and configureForLandscape() are called from elsewhere in the code. – theAlse Jan 13 '17 at 09:06
  • Without a [MCVE] it's almost impossible to find the problem. On the other hand, creating such a [MCVE] is not very hard. In my opinion, if you can't be bothered to produce one, why should anyone be bothered to find a solution for you. – ImportanceOfBeingErnest Jan 13 '17 at 09:13
  • @ImportanceOfBeingErnest please see edit, I added a minimal, complete example – theAlse Jan 13 '17 at 11:13

2 Answers2

1

I managed to solve this issue, or to be exact I figured out a around for this issue. I am adding it below and maybe it will help someone else in the future. I am basically using the queue to communicate when the graph should be updated in the main thread.

import matplotlib
matplotlib.use('TkAgg')
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg, NavigationToolbar2TkAgg
import multiprocessing
from Tkinter import *
import matplotlib.path as path
import matplotlib.patches as patches
import matplotlib.pyplot as plt
import sys, thread, time

from random import randint

#Create a window
window=Tk()

sendProximityInfo = True
latest_published_msg = ""

monitor_box = path.Path([(1000, -1000), (111, -213), (62, -220), (800, 1734)])
fig = matplotlib.figure.Figure()
ax  = fig.add_subplot(1,1,1)
ax.set_xlim(-1500,2000)
ax.set_ylim(-1500,2000)
patch = patches.PathPatch(monitor_box, facecolor='black', lw=1)
ax.add_patch(patch)

def main():
    #Create a queue to share data between process
    q = multiprocessing.Queue()
    #Create and start the simulation process
    simulate = multiprocessing.Process(target=simulation,args=(q,))
    simulate.start()   
    #Create the base plot
    plot()
    #Call a function to update the plot when there is new data
    updateplot(q)

    window.mainloop()
    print 'Done'
    simulate.join() # wait for the other process to finish as well

def plot():    #Function to create the base plot, make sure to make global the lines, axes, canvas and any part that you would want to update later
    global line, ax, canvas, fig, monitor_box, patch
    patch.remove()
    monitor_box = path.Path([(500, -500), (111, -213), (62, -220), (800, 1734)])
    patch = patches.PathPatch(monitor_box, facecolor='pink', lw=1)
    ax.add_patch(patch)

    canvas = FigureCanvasTkAgg(fig, master=window)
    canvas.show()
    canvas.get_tk_widget().pack(side=TOP, fill=BOTH, expand=1)
    canvas._tkcanvas.pack(side=TOP, fill=BOTH, expand=1)
    line, = ax.plot([], [], 'ro')

def erasePatchesAndCreateNew_A():
    print "erasePatchesAndCreateNew_A"
    global ax, monitor_box, patch
    patch.remove()
    monitor_box = path.Path([(35, 1677), (11, -213), (652, -220), (500, 1734)])
    patch = patches.PathPatch(monitor_box, facecolor='red', lw=1)
    ax.add_patch(patch)


def erasePatchesAndCreateNew_B():
    print "erasePatchesAndCreateNew_B"
    global ax, monitor_box, patch
    patch.remove()
    monitor_box = path.Path([(-2000, 2000), (11, -213), (300, -220), (500, 1734)])
    patch = patches.PathPatch(monitor_box, facecolor='blue', lw=1)
    ax.add_patch(patch)


def updateplot(q):
    try:       #Try to check if there is data in the queue
        result = q.get_nowait()

        if result != 'A' and result != 'B':
            x, y = result
            line.set_data(x, y)
            ax.draw_artist(line)
            canvas.draw()
            window.after(10,updateplot,q)
        elif result == 'A':
            erasePatchesAndCreateNew_A()
            canvas.draw()
            window.after(10,updateplot,q)
        elif result == 'B':
            erasePatchesAndCreateNew_B()
            canvas.draw()
            window.after(10,updateplot,q)
    except:
        window.after(10,updateplot,q)

def simulation(q):   
    try:
        while True:
            for i in range(5):
                q.put( (randint(0,1500), randint(0,1500)) )
                time.sleep(0.5)
            #erasePatchesAndCreateNew_A()
            q.put('A')
            time.sleep(1)
            for i in range(5):
                q.put( (randint(0,1500), randint(0,1500)) )
                time.sleep(0.5)
            #erasePatchesAndCreateNew_B()
            q.put('B')
            time.sleep(1)
    except KeyboardInterrupt:
        print "received KeyboardInterrupt"
    finally:
        print "simulation ended"
        sys.exit()

if __name__ == '__main__':
    main()
theAlse
  • 5,577
  • 11
  • 68
  • 110
1

I attempted to understand your problem to see if I could resolve it. I managed to replace the patch plots in a LIVE matplotlib chat upon user request. My working code is shown below.

While digesting your problem, I thought the multiprocessing section was a bit of a distraction to the main problem, i.e. unable to refresh the "matplotlib patches.Path" plot that is embedded in a tkinter window. As such, I adopted the LIVE matplotlib Chart solution by Sentdex (referenced in my script), which is quite similar to your code, as a basis to study your main problem. I think his approach to generating live plot is straight forward to understand.

So in my code, while the tkinter window is up, live data is being stream into the tkinter window LIVE chart, it reads the coordinates of the "matplotlib patches.Path" plot straight off the the file "patchesCoor.txt" and updates the LIVE chart with it. You have to create "patchesCoor.txt" at your end in the same folder as the script and it should contain a single row of entry comprising of 8 numbers separated by white space. Every time you amend the coordinate in that file and save it, the changes will appear in the LIVE Chart in the tkinter window. My solution excludes multiprocessing, although that can be implemented as the next phase of this script.

The section responsible for updating the plots in the LIVE chart is found in "def animate(i)." See my comments there.

Hope you find this solution useful, although it came a few hour after your posted answer. :)

#!/usr/bin/python3.5
# -*- coding: utf-8 -*-
"""
1. Script for creating LIVE matplotlib figure inside a tkinter window via 
   tkinter backend TkAgg (see Reference 1). I replaced the Pack system with a
   Grid system for the Tk objects (see Reference 2), created the scripts to input 
   the live data and added your random coordinate generator. 

2. It requires 1 input file:
   patchesCoor.txt - 4 x,y coordinate points for the patches.PathPatch plot
                     space seperated type.

References:
1. https://www.youtube.com/watch?v=Zw6M-BnAPP0
2. http://stackoverflow.com/questions/12913854/displaying-matplotlib-navigation-toolbar-in-tkinter-via-grid

Author: Sun Bear
Created on: 17 Jan 2017
"""

import matplotlib
matplotlib.use('TkAgg') # Backend of matplotlib
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg, NavigationToolbar2TkAgg
from matplotlib.figure import Figure
import matplotlib.patches as patches
import matplotlib.path as path
import matplotlib.animation as animation
from matplotlib import style
style.use('ggplot')

try:
    # for Python2
    import Tkinter as tk   ## notice capitalized T in Tkinter 
    import ttk
except ImportError:
    # for Python3
    import tkinter as tk
    import tkinter.ttk as ttk 

import random


class App(ttk.Frame):
    ''' Create tkinter window frame with base matplotlib figure, subplot and
        toolbar. '''

    def __init__(self, parent, *args, **kwargs):

        # Customise ttk styles
        s=ttk.Style()
        s.configure(".", font=('URW Gothic L', '11', 'bold'), foreground='#3D3C3A',
                    cap=tk.ROUND, join=tk.ROUND)
        s.configure('App.TFrame', background='pink')

        # Initialise App Frame
        ttk.Frame.__init__(self, parent, style='App.TFrame', borderwidth=20,
                           relief=tk.FLAT)
        self.grid(row=0, column=0, sticky='nsew')

        # Create tk Canvas
        canvas = FigureCanvasTkAgg(f, self)
        canvas.show()
        canvas.get_tk_widget().grid(row=0, column=0, sticky='nsew')

        # Create matplotlib navigation toolbar in a grid frame
        toolbar_frame = ttk.Frame(self, style='App.TFrame', borderwidth=2,
                           relief=tk.RAISED)
        toolbar_frame.grid(row=1, column=0, sticky='nsew')
        toolbar = NavigationToolbar2TkAgg(canvas, toolbar_frame)

        root.rowconfigure(0, weight=1)
        root.columnconfigure(0, weight=1)
        self.rowconfigure(0, weight=1)
        self.columnconfigure(0, weight=1)

def animate(i):
    '''Provide matplotlib figure with latest plots coordinates and refresh 
       matplotlib figure.'''

    # 1. Obtain x, y coordinates for Live data plot
    xList, yList = simulate()

    # 2. Obtain x, y coordinates for patches.PathPatch plot
    patchesData = open('patchesCoor.txt', 'r').read()
    print('patchesData = {}'.format(patchesData))
    patchesList = patchesData.split()
    print('patchesList = {}'.format(patchesList))
    if len(patchesList) > 1:
        x1,y1,x2,y2,x3,y3,x4,y4 = tuple(int(x) for x in patchesList)
    print('patchesCoor = {0} {1} {2} {3} {4} {5} {6} {7}'.
          format(x1,y1,x2,y2,x3,y3,x4,y4))
    monitor_box = path.Path([(x1, y1), (x2, y2), (x3, y3), (x4, y4)])
    patch = patches.PathPatch(monitor_box, facecolor='blue', lw=1)

    # 3. Clear LIVE Chart and update it with latest plot data
    ax.clear()
    ax.plot(xList, yList)   # For random x, y data plot 
    ax.add_patch(patch)     # For patches.PathPatch plot
    ax.set_xlim(-1500,2000) #   Need the following 2 lines to ensure 
    ax.set_ylim(-1500,2000) #   the Live Chart axis is updated at every read

def simulate():
    ''' Generate random x, y coordinate for Live data'''
    xList = []
    yList = []
    for i in range(100):
        x, y = random.randint(0,1500), random.randint(0,1500)
        xList.append(int(x))
        yList.append(int(y))
    return xList, yList

def matplotlib_base_figure():
    ''' Create matplotlib base figure '''
    f = Figure(figsize=(5,5), dpi=100)
    ax = f.add_subplot(111) # One chart created
    ax.plot([1,2,3,4,5,6,7,8], [5,6,1,8,6,10,2,7])
    monitor_box = path.Path([(35, 1677), (111, -213), (62, -220), (800, 1734)])
    patch = patches.PathPatch(monitor_box, facecolor='black', lw=1)
    ax.add_patch(patch)
    ax.set_xlim(-1500,2000)
    ax.set_ylim(-1500,2000)
    return f, ax 

if __name__ == '__main__':

    # 1. Create matplotlib base figure and subplot
    f, ax = matplotlib_base_figure()

    # 2. Create tkinter GUI with matplotlib base figure, subplot & toolbar.
    root = tk.Tk()
    app = App(root)

    # 3. Make matplotlib figure LIVE via animation
    ani = animation.FuncAnimation(f, animate, interval=1000)
    # 'interval' control the update rate of the LIVE Chart. 

    # 4. Activate GUI continually via an infinite loop.
    app.mainloop()

Original: Before Updated:Updated

Sun Bear
  • 7,594
  • 11
  • 56
  • 102