"""A resuable panel for displaying graphs.
This module provides a panel which can be used in frames to display graphs to
the user. It uses matplotlib, and it probably is not the fastest (as in
processing speed) way to implement this, but it works for the most part.
"""
import logging
from matplotlib.figure import Figure
from matplotlib.backends.backend_wxagg import FigureCanvasWxAgg
from matplotlib.backends.backend_wxagg import NavigationToolbar2WxAgg
import numpy as np
import pylab
import Queue
import wx
from src.core.configuration import c
from src.gui import images as img
log = logging.getLogger('transport')
COLORS = c.getGraphColors()
NUMCOL = len(COLORS)
ASCHECK = wx.ID_ANY
_ID_FIT = wx.NewId()
_ID_ZOOM = wx.NewId()
_ID_PAN = wx.NewId()
_ID_LOCK = wx.NewId()
[docs]class GraphPanel(wx.Panel):
"""A simple panel for displaying a single, multi-plot graph.
This is a simple panel for displaying graphical data to the user. It
interacts with the relevant `Experiment` and `Graph` objects through a
`Queue` instance.
Parameters
----------
parent : wxWindow
The window (frame or panel) which contains this panel.
labels : 2-tuple of str
A two-string sequence of strings representing the x- and y-axis labels
for the graph.
title : str
If not `None`, the title that goes above the graph.
dataQueue : Queue
An instance of `Queue.Queue` used to transfer data between a `Graph`
object and this panel.
"""
def __init__(self, parent, labels, title=None, dataQueue=None):
super(GraphPanel, self).__init__(parent, wx.ID_ANY)
self.SetMinSize((400, 300))
self.dataQueue = dataQueue
# The data for the CURRENT plot (numeric data)
self.xdata = []
self.ydata = []
# The data for ALL plots (a list of axes objects)
self.plotData = []
# Which index in self.plotData the new points should be added to
self.currentPlot = 0
# Range-related data
self.minx = None
self.maxx = None
self.miny = None
self.maxy = None
self.dpi = 100
self.fig = None
self.canvas = None
self.updatesEnabled = True
# Create a figure object
try:
self.fig = Figure((3.0, 3.0), dpi=self.dpi, tight_layout=True)
except TypeError:
log.error('Tight layout probably is not implemented in ' +
'this version of matplotlib')
self.fig = Figure((3.0, 3.0), dpi=self.dpi)
backgroundColor = []
for item in self.GetBackgroundColour().Get():
backgroundColor.append(item/255.0)
self.fig.patch.set_facecolor(backgroundColor)
# Create an axes object by adding a "subplot" to the figure, then format
self.axes = self.__constructInitialAxes(labels, title)
# Create a new plot and append it to the plotData array.
self.plotData.append(
self.axes.plot(self.xdata, self.ydata,
linewidth=1, marker='s',
color=COLORS[0])[0])
# Set the starting range
self.axes.set_xbound(lower=-0.1, upper=7)
self.axes.set_ybound(lower=-1.1, upper=1.1)
self.canvas = FigureCanvasWxAgg(self, -1, self.fig)
# Create the toolbar
self.toolbar = self.__constructToolbar(self.canvas)
self.toolbar.Realize()
# Put it all together.
vbox = wx.BoxSizer(wx.VERTICAL)
vbox.Add(self.canvas, 1, flag=wx.LEFT | wx.TOP | wx.GROW)
vbox.Add(self.toolbar, 0, wx.ALIGN_CENTER_HORIZONTAL, 5)
self.SetSizer(vbox)
def __constructToolbar(self, canvas):
"""Create the toolbar.
Parameters
----------
canvas : FigureCanvasWxAgg
The canvas (from matplotlib) which should be controlled by the
new toolbar.
Returns
-------
wxToolbar
The new toolbar.
"""
chartToolbar = NavigationToolbar2WxAgg(canvas)
chartToolbar.Show(False)
myToolbar = wx.ToolBar(self)
myToolbar.AddTool(_ID_FIT, img.getGraphFitBitmap(),
shortHelpString='Fit data')
myToolbar.AddSeparator()
myToolbar.AddRadioTool(_ID_ZOOM, img.getGraphZoomBitmap(),
shortHelp='Zoom')
myToolbar.AddRadioTool(_ID_PAN, img.getGraphMoveBitmap(),
shortHelp='Pan')
myToolbar.ToggleTool(_ID_PAN, True)
chartToolbar.pan()
myToolbar.AddSeparator()
myToolbar.AddCheckTool(_ID_LOCK, img.getGraphUnlockBitmap(),
img.getGraphLockBitmap(),
'Enable/disable updates.')
self.Bind(wx.EVT_TOOL, self.drawPlot, id=_ID_FIT)
self.Bind(wx.EVT_TOOL, chartToolbar.zoom, id=_ID_ZOOM)
self.Bind(wx.EVT_TOOL, chartToolbar.pan, id=_ID_PAN)
self.Bind(wx.EVT_TOOL, self.toggleUpdates, id=_ID_LOCK)
return myToolbar
def __constructInitialAxes(self, labels, title=None):
"""Construct axes.
Parameters
----------
labels : tuple of str
A tuple of two strings specifying the labels for the x- and y-axes,
respectively.
title : str
The title for the graph. If `None` (the default), no title will be
shown.
Returns
-------
axes
A newly created matplotlib axes object.
"""
axes = self.fig.add_subplot(111)
axes.set_axis_bgcolor('black')
axes.grid(True, color='gray')
axes.set_xlabel(labels[0], fontsize=9)
axes.set_ylabel(labels[1], fontsize=9)
if title is not None:
axes.set_title(title, fontsize=9)
pylab.setp(axes.get_xticklabels(), fontsize=8)
pylab.setp(axes.get_yticklabels(), fontsize=8)
return axes
[docs] def toggleUpdates(self, event=None):
"""Enable updates to the graph."""
newState = not self.toolbar.GetToolState(_ID_LOCK)
if newState:
self.toolbar.SetToolNormalBitmap(_ID_LOCK,
img.getGraphUnlockBitmap())
self.updatesEnabled = True
self.onUpdate(None)
else:
self.toolbar.SetToolNormalBitmap(_ID_LOCK,
img.getGraphLockBitmap())
self.updatesEnabled = False
[docs] def setDataQueue(self, dataQueue):
"""Set the data queue."""
self.dataQueue = dataQueue
[docs] def onUpdate(self, event):
"""Update the plot.
Query the queue, popping points from it, until it is empty. Then draw
the graph with the new data.
"""
if self.updatesEnabled:
try:
while not self.dataQueue.empty():
newpoint = self.dataQueue.get()
self.addPoint(newpoint)
self.drawPlot()
except Queue.Empty:
pass
[docs] def drawPlot(self, event=None):
"""Redraw the plot."""
if self.minx is not None:
padx = (self.maxx - self.minx)*0.05
pady = (self.maxy - self.miny)*0.05
self.axes.set_xbound(lower=self.minx-padx, upper=self.maxx+padx)
self.axes.set_ybound(lower=self.miny-pady, upper=self.maxy+pady)
pylab.setp(self.axes.get_xticklabels(), visible=True)
self.plotData[self.currentPlot].set_xdata(np.array(self.xdata))
self.plotData[self.currentPlot].set_ydata(np.array(self.ydata))
self.canvas.draw()
[docs] def addPoint(self, data):
"""Add a new point to the graph.
Parameters
----------
data : 6-tuple of float
A tuple which consists of the following values (all floats) in
order:
- the x-value of the point to add,
- the y-value of the point to add,
- the minimum x-value in the graph,
- the maximum x-value in the graph,
- the minimum y-value in the graph, and
- the maximum y-value in the graph.
"""
if data[-1]:
self.addPlot()
self.xdata.append(data[0])
self.ydata.append(data[1])
self.minx = data[2]
self.maxx = data[3]
self.miny = data[4]
self.maxy = data[5]
[docs] def addPlot(self):
"""Add a new plot to the current graph."""
self.xdata = []
self.ydata = []
self.currentPlot += 1
self.plotData.append(
self.axes.plot(self.xdata, self.ydata,
linewidth=1, marker='s',
color=COLORS[self.currentPlot%NUMCOL])[0])
[docs] def onExit(self, event):
"""Destroy the panel."""
self.Destroy()