"""A container for graphical data.
A `Graph` is an object to facilitate interactions between an `Experiment`
and an actual GUI implementation of graphing.
This module provides the following classes:
Graph:
An interface for passing data from an experiment to a visual graph.
AbstractGraphManager:
An interface for spawning graph threads; it (and all its methods) must be
overridden to actually see anything.
"""
from abc import ABCMeta, abstractmethod
import logging
from src.tools.parsing import escapeXML
log = logging.getLogger('transport')
#------------------------------------------------------- Graph manager interface
[docs]class AbstractGraphManager(object):
"""An abstract manager for graphs.
The purpose of a `GraphManager` is to take as input a list of `Graph`
objects and, as the experiment is preparing to run, create a frame or
frames to hold the graphical representation of the graphs and start
threads to update them as data become available.
Parameters
----------
parentFrame
A GUI frame to pass as the parent of each graph frame.
"""
__metaclass__ = ABCMeta
@abstractmethod
def __init__(self, parentFrame):
"""Initialize a GraphManager."""
@abstractmethod
[docs] def setGraphs(self, graphs):
"""Set the graphs to be managed.
Parameters
----------
graphs : list of `Graph`
A list of `Graph` objects.
"""
@abstractmethod
[docs] def abort(self, timeout=10.0):
"""Stop graphing.
Command all `Graph` objects to stop taking new data.
Parameters
----------
timeout : float
The maximum time to wait for the threads to join.
"""
@abstractmethod
[docs] def start(self):
"""Start graphing.
Start a thread for each `Graph` object. These should be spawned as
'daemon' threads, so that if the program is exited, the threads all
stop.
"""
@abstractmethod
[docs] def saveGraphs(self, filename):
"""Save the graphs to a file or files.
Parameters
----------
filename : str or list
The output file(s) to which the graph(s) should be saved.
Implementations may require either a string or a list of strings.
"""
#------------------------------------------------------------------- Graph class
[docs]class Graph(object):
"""A `Graph` is an object for managing interactions between an
`Experiment` and some graphical interface for actually displaying the
data. It effects the data transfer by pushing data points onto a `Queue`
object.
Parameters
----------
experiment : Experiment
The `Experiment` object which owns this `Graph`.
colx : str
The name of the column from which the x coordinates come.
coly : str
The name of the column from which the y coordinates come.
colAdd : str
The name of the column which, when updated, should signal that the next
point added to the graph should begin a new plot.
Notes
-----
The `Graph` object does not actually store all of the data---it only keeps
track of the latest point. The reason is that nearly every GUI
implementation of graphing needs to keep track of all of the data, so
`Graph` and the graphing panel/frame contain it would double the
memory used with no real advantage.
"""
def __init__(self, experiment, colX, colY, colAdd):
"""Create a new graph."""
self._expt = experiment
self._nx = None
self._ny = None
self._flagNew = False
self._colX = colX
self._colY = colY
self._colAdd = colAdd
self._dataQueue = None
self._minX = None
self._maxX = None
self._minY = None
self._maxY = None
self._enabled = True
if __debug__:
log.debug('Creating a new graph: %s vs %s.', colY, colX)
[docs] def setEnabled(self, enabled):
"""Set whether this graph is enabled.
The enabled state determines only whether or not the experiment adds
it to the manager. The `Graph` object itself does not use the flag at
all.
Parameters
----------
enabled : bool
Whether the graph should be enabled.
"""
self._enabled = enabled
[docs] def isEnabled(self):
"""Return whether the graph is enabled.
The enabled state determines only whether or not the experiment adds
it to the manager. The `Graph` object itself does not use the flag at
all.
Returns
-------
bool
Whether the graph is enabled.
"""
return self._enabled
[docs] def setDataQueue(self, queue):
"""Set the queue to pass data to the UI.
A queue is used to pass data from this graph object to the graphical
component which actually displays the data.
Parameters
----------
queue : Queue.Queue
The queue to pass data to the user interface.
"""
self._dataQueue = queue
[docs] def flagNewPlot(self):
"""Indicate that the next point should go into a new plot."""
self._flagNew = True
[docs] def clear(self):
"""Reset the graph data."""
self._minX = None
self._maxX = None
self._minY = None
self._maxY = None
self._nx = None
self._ny = None
self._flagNew = False
[docs] def addX(self, newx):
"""Add the x-coordinate of the next point to the graph.
Parameters
----------
newx : float
The new x-value to add to the graph. If it is an integer or a
string, it will be coerced to a float.
"""
self._nx = float(newx)
if self._minX == None:
self._minX = self._nx-0.2
self._maxX = self._nx+0.2
elif self._nx > self._maxX:
self._maxX = self._nx
elif self._nx < self._minX:
self._minX = self._nx
self.checkPoint()
[docs] def addY(self, newy):
"""Add the y-coordinate of the next point to the graph.
Parameters
----------
newy : float
The new y-value to add to the graph. If it is an integer or a
string, it will be coerced to a float.
"""
self._ny = float(newy)
if self._minY == None:
self._minY = self._ny-0.2
self._maxY = self._ny+0.2
elif self._ny > self._maxY:
self._maxY = self._ny
elif self._ny < self._minY:
self._minY = self._ny
self.checkPoint()
[docs] def checkPoint(self):
"""Add a point to the graph, if appropriate.
Check whether there is both an x- and a y-coordinate available. If so,
add a tuple to the queue, where the tuple contains the following
elements in order:
- New x-coordinate
- New y-coordinate
- Minimum x-coordinate for the graph
- Maximum x-coordinate for the graph
- Minimum y-coordinate for the graph
- Maximum y-coordinate for the graph
- Whether this point goes into a new plot
Then reset the graph's fields.
"""
if (self._nx is not None and
self._ny is not None and
self._dataQueue is not None):
self._dataQueue.put((self._nx, self._ny,
self._minX, self._maxX,
self._minY, self._maxY,
self._flagNew))
self._flagNew = False
self._ny = None
[docs] def getColumns(self):
"""Return the column names which will provide data to this graph.
Returns
-------
tuple of str
A 3-tuple whose contents are, in order, the names of the x data
column, the y data column, and the column to trigger new plots on
the graph.
"""
return (self._colX, self._colY, self._colAdd)
[docs] def setColumns(self, cols):
"""Set the names of the columns which will provide data to this graph.
Parameters
----------
cols : tuple of str
A 3-tuple whose contents indicate the names of the x-column, the
y-column, and the column which should trigger new plots on the
graph, in that order.
"""
if __debug__:
log.debug('Setting columns to ' + str(cols))
self._colX = cols[0]
self._colY = cols[1]
self._colAdd = cols[2]
[docs] def getTitle(self):
"""Get the title of the graph ("y name vs. x name").
Returns
-------
str
The name of the graph, in the form "y-column name vs. x-column
name".
"""
title = self._colY + ' vs. ' + self._colX
return title
def __str__(self):
"""Get the title of the graph ("y name vs. x name").
Returns
-------
str
The name of the graph, in the form "y-column name vs. x-column
name".
"""
return self.getTitle()
[docs] def updateColumnsIfNecessary(self, oldcol, newcol):
"""Update graph labels to reflect changes in column names.
Parameters
----------
oldcol : str
The name of the column from which the graph previously received
data.
newcol : str
The name of the column from which the graph should now receive
data.
"""
if self._colX == oldcol:
self._colX = newcol
if self._colY == oldcol:
self._colY = newcol
if self._colAdd == oldcol:
self._colAdd = newcol
#---------------------------------------------------- Graph data persistence
def __getstate__(self):
"""Return the parts of the `Graph` which are important when saving.
Returns
-------
dict
The data dictionary of the `Graph` object with the un-picklable
(and otherwise undesired) elements removed.
"""
odict = self.__dict__.copy()
odict['_dataQueue'] = None
return odict
[docs] def getXML(self, indent=0):
"""Return an XML string representing the graph.
Returns
-------
str
A string containing XML data representing all of the data related
to the graph, possibly useful as an alternative to pickle.
"""
return (' '*indent +
'<graph xcol="%s" ycol="%s" addcol="%s" enabled="%s" />' %
(escapeXML(self._colX), escapeXML(self._colY),
repr(self._colAdd), repr(self._enabled)))