Adam Barnes
Adam Barnes

Reputation: 3232

matplotlib.pyplot in a wx.Panel

In the .__init__() for this class, I have the lines:

class report_order_totals_by_photographer(wx.Panel):
    def __init__(...):
        ...
        self.figure = matplotlib.figure.Figure()
        self.canvas = matplotlib.backends.backend_wxagg.FigureCanvasWxAgg(panel_canvas, -1, self.figure)

And in an event handler I have:

    self.figure.clf()

    datapoints = range(len(self.data))
    values = [x[1] for x in self.data]
    labelpositions = [x + 0.5 for x in range(len(self.data))]
    labeltext = [x[0] for x in self.data]

    bars = matplotlib.pyplot.barh(datapoints, values)
    labels = matplotlib.pyplot.yticks(labelpositions, labeltext)

    self.figure.artists.extend(bars)
    self.figure.axes.extend(labels)

    self.canvas.draw()

Which looks like this:

https://i.sstatic.net/Br7vW.png

Or this when resized:

https://i.sstatic.net/wnMku.png

Which is confusing and frustrating. I want it to look something like this:

data = db.get_order_totals_for_photographers(*season_to_dates("14w"))
import matplotlib.pyplot as plt
plt.barh(range(len(data)), [x[1] for x in data])
plt.yticks([x + 0.5 for x in range(len(data))], [y[0] for y in data])
plt.show()

i.imgur.com/BD1RXOq.png

(Which is also confusing and frustrating, 'cause the names extend off the page, but whatever, I can care about that later).

My main two problems are that it's not resizing with the frame it's in, and that I can only draw the bars, and nothing else.

In my working example, I'm using what the matplotlib tutorial calls a "thin stateful wrapper around matplotlib's API", which weirds me out, and I don't like it. Luckily, the tutorial follows up with: "If you find this statefulness annoying, don't despair, this is just a thin stateful wrapper around an object oriented API, which you can use instead (See Artist tutorial)". In the artist tutorial, there's very little useful information which I could use, but a couple hints, that lead me to self.figure.artists.extend(bars).

I've had slightly more success with self.figure.texts.extend(labels[1]), but that just piles all the names on top of eachother, (and I think the ticks are piled on top of eachother right of the plot), so I'm still clueless.

Any help would be greatly appreciated.

Upvotes: 0

Views: 1632

Answers (2)

Adam Barnes
Adam Barnes

Reputation: 3232

God damn that was harder than it needed to be. Here's a self-contained example.

(I have since created a repository with much nicer code).

import wx
import matplotlib.backends.backend_wxagg
import matplotlib.pyplot
import matplotlib.figure
import numpy

data = [
    [
        ["Arse", 5],
        ["Genitals", 12],
        ["Bum", 2]
        ],
    [
        ["Fart", 3],
        ["Guff", 2],
        ["EXPLOSION", 6]
        ],
    [
        ["Clam", 2],
        ["Stench trench", 7],
        ["Axe wound", 3],
        ["Fourth euphemism", 9]
        ]
    ]

class panel_plot(wx.Panel):
    def __init__(self, parent):
        wx.Panel.__init__(self, parent, style = wx.NO_FULL_REPAINT_ON_RESIZE)

        self.figure = matplotlib.figure.Figure()
        self.canvas = matplotlib.backends.backend_wxagg.FigureCanvasWxAgg(self, -1, self.figure)

        self.set_size()
        self.draw()

        self._resize_flag = False

        self.Bind(wx.EVT_IDLE, self.on_idle)
        self.Bind(wx.EVT_SIZE, self.on_size)

    def on_idle(self, event):
        if self._resize_flag:
            self._resize_flag = False
            self.set_size()

    def on_size(self, event):
        self._resize_flag = True

    def set_size(self):
        pixels = tuple(self.GetSize())
        self.SetSize(pixels)
        self.canvas.SetSize(pixels)
        self.figure.set_size_inches([float(x) / self.figure.get_dpi() for x in pixels])

    def draw(self):
        self.canvas.draw()

class frame_main ( wx.Frame ):
    def __init__( self, parent ):
        wx.Frame.__init__ ( self, parent, id = wx.ID_ANY, title = wx.EmptyString, pos = wx.DefaultPosition, size = wx.Size( 500,300 ), style = wx.DEFAULT_FRAME_STYLE|wx.TAB_TRAVERSAL )

        self.SetSizeHintsSz( wx.DefaultSize, wx.DefaultSize )

        sizer_bg = wx.BoxSizer( wx.VERTICAL )

        self.panel_bg = wx.Panel( self, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.TAB_TRAVERSAL )
        sizer_main = wx.BoxSizer( wx.VERTICAL )

        self.button_change = wx.Button( self.panel_bg, wx.ID_ANY, u"Change data", wx.DefaultPosition, wx.DefaultSize, 0 )
        sizer_main.Add( self.button_change, 0, wx.ALL, 5 )

        self.panel_matplotlib = panel_plot(self.panel_bg)
        sizer_main.Add( self.panel_matplotlib, 1, wx.EXPAND |wx.ALL, 5 )


        self.panel_bg.SetSizer( sizer_main )
        self.panel_bg.Layout()
        sizer_main.Fit( self.panel_bg )
        sizer_bg.Add( self.panel_bg, 1, wx.EXPAND, 5 )


        self.SetSizer( sizer_bg )
        self.Layout()

        self.i = 0

        self.on_change()

        self.setbinds()

    def setbinds(self):
        self.button_change.Bind(wx.EVT_BUTTON, self.on_change)

    def on_change(self, event = None):
        self.panel_matplotlib.figure.clf()
        axes = self.panel_matplotlib.figure.gca()

        self.i += 1
        _data = numpy.array(data[self.i%3])

        datapoints = numpy.array(range(len(_data)))
        values = numpy.array([int(x[1]) for x in _data])
        labelpositions = numpy.array([x + 0.4 for x in range(len(_data))])
        labeltext = numpy.array([x[0] for x in _data])

        axes.barh(datapoints, values)
        axes.set_yticks(labelpositions)
        axes.set_yticklabels(labeltext)

        self.panel_matplotlib.draw()


class App(wx.App):
    def __init__(self):
        super(App, self).__init__()
        self.mainframe = frame_main(None)
        self.mainframe.Show()
        self.MainLoop()

App()

Key points to consider that the articles I sourced didn't manage:

  1. Everything's gotta be a numpy array. matplotlib has a habit of using the + operator on arraylikes, and for python lists, that's just an extend.
  2. It looks like all of the dank functions in matplotlib.pyplot are also on axes, which is matplotlib.figure.Figure().gca().
  3. axes.set_yticks() needs to be done before .set_yticklabels, for the labels' position to be right.
  4. super(panel_plot, self).__init__(...) doesn't seem to work, for no immediately apparent reason.
  5. In panel_plot.set_size(), the first line needs to contain self.GetSize(), not self.Parent.GetClientSize(). GetClientSize() oversizes it a bit; I think it counts the drawable area, AND the border.

These are the articles I pieced together:

Upvotes: 2

HamsterHuey
HamsterHuey

Reputation: 1243

Couple of things - Have you read these sections?

http://matplotlib.org/faq/howto_faq.html#move-the-edge-of-an-axes-to-make-room-for-tick-labels

http://matplotlib.org/faq/howto_faq.html#automatically-make-room-for-tick-labels

They should likely help explain why you are having this issue and how to sort it. It does look like you will have to write the code as in the examples above to take care of layout and spacing of elements since you are updating labels after the fact which can throw off alignment and formatting.

Also, I think you need to take a step back and perhaps spend a bit of time familiarizing yourself with Matplotlib API and how the different Classes and Objects play together. Right now it looks like you're kinda trying to guess your way through, and as someone who has been there and done that, it is not going to end well :-). Once you understand the OOP structure of matplotlib and how things play together, you should have a better idea of how to tackle things.

Have you also read through these examples? http://matplotlib.org/examples/user_interfaces/index.html

I ran the QT example and it worked fine. Working through them should also give you an idea on how to structure things and how different elements play together.

Upvotes: 0

Related Questions