0

I'm writing a python application using pyside and matplotlib. Following a combination of this tutorial and this SO post, I have created a matplotlib widget that I can successfully add to a parent. However when I go to actually add data to it, nothing seems to get displayed.

If I add static data like the SO post had, it shows up, but when I change it to update on the fly (currently every second on a timer, but it will eventually be using a signal from another class), I never get anything but the empty axes to appear. I suspect that I'm missing a call to force a draw or invalidate or that there is something wrong with the way I'm calling update_datalim (though the values that get passed to it seem correct).

from PySide import QtCore, QtGui

import matplotlib
import random

matplotlib.use('Qt4Agg')
matplotlib.rcParams['backend.qt4']='PySide'

from matplotlib import pyplot as plt
from matplotlib.backends.backend_qt4agg import FigureCanvasQTAgg as FigureCanvas
from matplotlib.figure import Figure
from matplotlib.patches import Rectangle

from collections import namedtuple

DataModel = namedtuple('DataModel', ['start_x', 'start_y', 'width', 'height'])

class BaseWidget(FigureCanvas):

    def __init__(self, parent=None, width=5, height=4, dpi=100):
        fig = Figure(figsize=(width, height), dpi=dpi)
        self.axes = fig.add_subplot(111)
        # We want the axes cleared every time plot() is called
        self.axes.hold(False)
        self.axes.set_xlabel('X Label')
        self.axes.set_ylabel('Y Label')
        self.axes.set_title('My Data')

        FigureCanvas.__init__(self, fig)
        self.setParent(parent)

        FigureCanvas.setSizePolicy(self,
                                   QtGui.QSizePolicy.Expanding,
                                   QtGui.QSizePolicy.Expanding)

        FigureCanvas.updateGeometry(self)

class DynamicWidget(BaseWidget):
    def set_data(self, the_data):
        self.axes.clear()
        xys = list()

        cmap = plt.cm.hot
        for datum in the_data:
            bottom_left = (datum.start_x, datum.start_y)
            top_right = (bottom_left[0] + datum.width, bottom_left[1] + datum.height)
            rect = Rectangle(
                xy=bottom_left,
                width=datum.width, height=datum.height, color=cmap(100)
            )
            xys.append(bottom_left)
            xys.append(top_right)
            self.axes.add_artist(rect)
        self.axes.update_datalim(xys)
        self.axes.figure.canvas.draw_idle()

class RandomDataWidget(DynamicWidget):
    def __init__(self, *args, **kwargs):
        DynamicWidget.__init__(self, *args, **kwargs)
        timer = QtCore.QTimer(self)
        timer.timeout.connect(self.generate_and_set_data)
        timer.start(1000)

    def generate_and_set_data(self):
        fake_data = [DataModel(
            start_x=random.randint(1, 100),
            width=random.randint(20, 40),
            start_y=random.randint(80, 160),
            height=random.randint(20, 90)
        ) for i in range(100)]

        self.set_data(fake_data)

Edit: I'm suspecting that there's an issue with updating the limits of the plot. When running the above code, the plot opens with limits of 0 and 1 on both the x and y axis. Since none of my generated data falls into that range, I created another subclass of DynamicWidget that plots only data between 0 and 1 (the same data from the linked SO post). When instantiating the class below, the data shows up successfully. Do I need to do something more than calling update_datalim to get the graph to re-bound itself?

class StaticWidget(DynamicWidget):
    def __init__(self):
        DynamicWidget.__init__(self)
        static_data = [
            DataModel(0.5, 0.05, 0.2, 0.05),
            DataModel(0.1, 0.2, 0.7, 0.2),
            DataModel(0.3, 0.1, 0.8, 0.1)
        ]
        self.set_data(static_data)
Community
  • 1
  • 1
Dave McClelland
  • 3,385
  • 1
  • 29
  • 44

1 Answers1

2

Yes, update_datalim only updates the bounding box that is kept internally by the axes. You also need to enable auto scaling for it to be used. Add self.axes.autoscale(enable=True) after the self.axes.clear() statement and it will work. Or you can set the axes' range to a fixed value by using self.axes.set_xlim and self.axes.set_ylim.

edit: here is my code, which works for me

from PySide import QtCore, QtGui

import matplotlib
import random, sys

matplotlib.use('Qt4Agg')
matplotlib.rcParams['backend.qt4']='PySide'

from matplotlib import pyplot as plt
from matplotlib.backends.backend_qt4agg import FigureCanvasQTAgg as FigureCanvas
from matplotlib.figure import Figure
from matplotlib.patches import Rectangle

from collections import namedtuple

DataModel = namedtuple('DataModel', ['start_x', 'start_y', 'width', 'height'])

class BaseWidget(FigureCanvas):

    def __init__(self, parent=None, width=5, height=4, dpi=100):
        fig = Figure(figsize=(width, height), dpi=dpi)
        self.axes = fig.add_subplot(111)
        # We want the axes cleared every time plot() is called
        self.axes.hold(False)
        #self.axes.autoscale(enable=True)
        self.axes.set_xlabel('X Label')
        self.axes.set_ylabel('Y Label')
        self.axes.set_title('My Data')

        FigureCanvas.__init__(self, fig)
        self.setParent(parent)

        FigureCanvas.setSizePolicy(self,
                                   QtGui.QSizePolicy.Expanding,
                                   QtGui.QSizePolicy.Expanding)

        FigureCanvas.updateGeometry(self)

class DynamicWidget(BaseWidget):
    def set_data(self, the_data):
        self.axes.clear()
        self.axes.autoscale(enable=True)
        #self.axes.set_xlim(0, 300)
        #self.axes.set_ylim(0, 300)
        xys = list()

        cmap = plt.cm.hot
        for datum in the_data:
            print datum
            bottom_left = (datum.start_x, datum.start_y)
            top_right = (bottom_left[0] + datum.width, bottom_left[1] + datum.height)
            rect = Rectangle(
                xy=bottom_left,
                width=datum.width, height=datum.height, color=cmap(100)
            )
            xys.append(bottom_left)
            xys.append(top_right)
            self.axes.add_artist(rect)
        self.axes.update_datalim(xys)
        self.axes.figure.canvas.draw_idle()

class RandomDataWidget(DynamicWidget):
    def __init__(self, *args, **kwargs):
        DynamicWidget.__init__(self, *args, **kwargs)
        timer = QtCore.QTimer(self)
        timer.timeout.connect(self.generate_and_set_data)
        timer.start(1000)

    def generate_and_set_data(self):
        fake_data = [DataModel(
            start_x=random.randint(1, 100),
            width=random.randint(20, 40),
            start_y=random.randint(80, 160),
            height=random.randint(20, 90)) for i in range(100)]

        self.set_data(fake_data)
        print "done:...\n\n"


def main():
    qApp = QtGui.QApplication(sys.argv)

    aw = RandomDataWidget()

    aw.show()
    aw.raise_()
    sys.exit(qApp.exec_())

if __name__ == "__main__":
    main()
titusjan
  • 5,376
  • 2
  • 24
  • 43
  • Adding only `autoscale(enable=True)` hasn't seemed to make it work for me. I appreciate the help, though. Any other ideas? – Dave McClelland Apr 04 '16 at 13:32
  • Be sure to add it after `axes.clear`, otherwise it will be reset. Or perhaps try to use `set_xlim` and `set_ylim`. In any case I have edited my answer to include my adapted code. This works for me so I would be very surprised if it doesn't work for you. Can you try it? – titusjan Apr 04 '16 at 14:14
  • I think I'm an idiot - I had removed the `draw_idle` call whenever it hadn't worked for me. It's working now, thank you! – Dave McClelland Apr 04 '16 at 14:31