"""
Live display of 2D data (e.g. images) and 1D data (e.g. spectra) for MicroscoPy
This classes are not meant to be used directly but subclassed for specific
purpose/hardware. In particular, :class:`LivePlot` must be extended with
a :func:`get_data` method that returns the data to be plotted.
Widgets
-------
Extra widgets are added to LivePlot with the widgets keywords, for examples::
widgets = [[w1,w2,w3], # one sublist per row
[w4], # widgets in sublist go in column
[w5,w6]]
will add six widgets (in three rows) below the main figure.
w1, w2, ... are not the widgets themselves, but functions that will be called
by the LivePlot constructor, and must take exactly two arguments:
- a Tkinter object used as parent for the new widget (so the widget knows where to go)
- a reference to the LivePlot object (so the widget can interact with the application)
In addition we may want the widget to interact objects outside the LivePlot.
This can be achieved thus::
def create_widget_X(*args, **kwargs):
def inner(parent, display):
return Widget_X(parent, [display], *args, **kwargs)
return inner
and then w1 above would be::
widgets = [[create_widget_X(cam), ..], ...] #
"""
import numpy as np
import Tkinter as tk
import matplotlib as mpl
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg, NavigationToolbar2TkAgg
import matplotlib.pyplot as plt
[docs]class LivePlot(object):
"""Provides a window to display 1D data (eg profiles across images or when in Single-Track/FVB acquisition mode).
To use it you must either
1) Provide a function to the data_func keyword argument, or
2) Sub-class LivePlot and define a get_data() method.
Both data_func and get_data must return a 2D array od dimensions (ntracks, npoints).
In addition, the following methods can be overriden if necessary:
- init_plot
- if_new_data_ready
- title
All tracks will be displayed in the same plot.
Use start() to update the plot automatically (every 100ms by default, settable via the delay property).
"""
def __init__(self, ntracks, npoints, data_func=None, master=None, widgets=None):
"""Create a pyplot window and initialise the plot with zeros.
:param ntracks: number of lines to display
:param npoints: line length
:param master: Tkinter parent object (will create a Toplevel if provided,
otherwise a new Tkinter instance)
:param data_func: function that return data as a numpy array of shape (ntracks, npoints).
Alternative to overriding :func:`get_data`
:param widgets: a nested list of optional widgets to add to the plot.
"""
if master is None:
self.window = tk.Tk()
else:
self.window = tk.Toplevel(master)
self.live = False
self.data_func = data_func
# Create figure
plt.ion()
self._init_app(widgets)
self.init_fig(ntracks, npoints)
# Stuff for live updating
self.delay = 100 # refresh rate, in ms.
self._wheel = ('-',"\\",'|','/')
self.count = 0
self._if_new_data_ready_counter = 0
self.last_image_read = None
self.transform = lambda x: x
def _init_app(self, extra):
"""Initialise the window with main plot area and widgets
:param extra: a (possibly nested) list of widgets.
"""
self.figure = mpl.figure.Figure()
self.ax = self.figure.add_subplot(111)
self.canvas = FigureCanvasTkAgg(self.figure, self.window)
self.tb_frame = tk.Frame(self.window)
self.toolbar = CustomToolbar(self.canvas, self.tb_frame, self.toggle)
self.toolbar.update()
self.widget = self.canvas.get_tk_widget()
self.widget.pack(side=tk.TOP, fill=tk.BOTH, expand=1)
self.toolbar.pack(side=tk.TOP, fill=tk.BOTH, expand=1)
self.extra_widgets = []
self.extra_widgets_update = []
self.extra_widgets_frames = []
if extra is not None:
for lines in extra:
self.extra_widgets_frames.append(tk.Frame(self.tb_frame))
for w in lines:
self.extra_widgets.append(w(self.extra_widgets_frames[-1], self))
self.extra_widgets[-1].pack(side=tk.LEFT)
try:
self.extra_widgets_update.append(self.extra_widgets[-1].refresh)
except AttributeError:
pass
#self.extra_widgets_side = tk.LEFT if self.extra_widgets[-1].top else tk.TOP
#self.extra_widgets_frames[-1].pack(side=self.extra_widgets_side)
self.extra_widgets_frames[-1].pack(side=tk.TOP, fill=tk.BOTH, expand=1, anchor=tk.W)
self.tb_frame.pack(side=tk.TOP, fill=tk.BOTH, expand=1)
self.canvas.show()
[docs] def init_fig(self, ntracks, npoints):
"""Initialise figure with n tracks of n points each."""
self.ntracks = ntracks
self.npoints = npoints
for i in range(self.ntracks): # create as many line plots as lines in the data set (eg, 2 if there are two tracks
self.ax.plot(np.arange(1, self.npoints+1), np.zeros(shape=self.npoints))
[docs] def title(self):
"""A title string for the plot window"""
return ""
#@if_new_data_ready # now taken care of by the get_data() method
[docs] def update(self):
"""Update the live plot."""
self.data = self.get_data()
if self.data is not None:
for i, line in enumerate(self.ax.lines):
line.set_ydata(self.transform(self.data[i])) # update ydata of each line
for w_func in self.extra_widgets_update:
w_func()
self.window.title(self.title())
self.canvas.draw()
[docs] def get_data(self):
"""Specifies how to get the data to be plotted.
It must either be overidden by subclasses or provided to the object constructor kwarg "data_func"
Return None if no new data is available.
"""
if self.data_func is None:
raise NotImplementedError, "No method to get data!"
else:
return self.data_func()
[docs] def start(self):
"""Start live updating of the plot."""
self.update()
self.callback_id = self.widget.after(int(self.delay), self.start)
self.live = True
self.count += 1
[docs] def stop(self):
"""Stop live updating."""
self.widget.after_cancel(self.callback_id)
self.live = False
[docs] def toggle(self):
"""Toggle live updating."""
if self.live:
self.stop()
else:
self.start()
def __del__(self):
self.stop()
@property
def autorefresh(self):
return self.live
@autorefresh.setter
def autorefresh(self, b):
if b and not self.live:
self.start()
elif not b and self.live:
self.stop()
###########################
# Widgets for LiveDisplay #
###########################
# - Widgets must be subclasses of Tkinter objects
# - If it has a refresh() method, it will be called by LiveDisplay.update()
# - LiveDisplay expects not the already created widget, but a function that will create
# the widget, with the following signature
# def create_widget_func(parent, display):
# return Widget_X(parent)
#
# To pass additional parameters to the widget, wrap the previous function:
# def create_widget_X(*args, **kwargs):
# def create_widget_func(parent, display):
# return Widget_X(parent, display, *args, **kwargs)
# return create_widget_func
# Add a status bar
class StatusWidget(tk.Label):
def __init__(self, master, status_var):
self.string = status_var # StatusWidgetVar object
self._var = tk.StringVar(master)
tk.Label.__init__(self, master, textvariable= self._var, text='No status info', anchor=tk.W)
def pack(self, **kwargs):
tk.Label.pack(self, fill=tk.BOTH, expand=1, **kwargs)
def refresh(self):
if self.string.new:
self._var.set(self.string.get())
# Add an exposure setting
# Add an autorefresh buttom
# Add a log/linear scale menu and offset