"""
Live display of 2D data (e.g. images) and 1D data (e.g. spectra).
"""
import matplotlib as mpl
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg, NavigationToolbar2TkAgg
import Tkinter as tk
import numpy as np
import matplotlib.pyplot as plt
class StatusWidget(tk.Label):
def __init__(self, master, status_var=None, top=True):
if status_var is None:
self._var = tk.StringVar(self)
else:
self._var = status_var
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)
class VerticalScaleWidget(tk.Frame):
def __init__(self, master, disp, top=True):
"""
:param master: the widget's parent
:param disp: the camera display (any object with a settable ``transform`` property)
"""
self._disp = disp
side = 0 if top else 1
self.top = top
tk.Frame.__init__(self, master)
self._saved_lin_scale = self._disp.ax.get_ylim()
self._scale_var = tk.StringVar(self)
self._offset_var = tk.IntVar(self)
#self._scale_var.trace('w', self._set_scale('scale'))
#self._offset_var.trace('w', self._set_scale('offset'))
self._scale_var.trace('w', self._set_scale)
self._offset_var.trace('w', self._set_offset)
self._scale_label = tk.Label(self, text='Scale')
self._scale_optionmenu = apply(tk.OptionMenu, (self, self._scale_var) + ('linear', 'log'))
self._scale_optionmenu.config(width=6)
self._scale_var.set('linear')
self._offset_label = tk.Label(self, text='Offset')
self._offset_entry = tk.Entry(self, textvariable=self._offset_var, width=5, justify=tk.RIGHT)
self._scale_label.grid(row=0, column=0)
self._scale_optionmenu.grid(row=1-side, column=0+side)
self._offset_label.grid(row=0, column=1+side)
self._offset_entry.grid(row=1-side, column=1+2*side)
def _set_scale(self, *args):
self._disp.ax.set_yscale(self._scale_var.get())
def _set_offset(self, *args):
try:
offset = self._offset_var.get()
self._disp.transform = lambda x: x - offset
except ValueError:
pass
def _set_scale_old(self, caller):
def _set_scale_inner(*args):
try:
offset = self._offset_var.get()
if self._scale_var.get() == 'linear':
self._disp.transform = lambda x: x - offset
else:
self._disp.transform = lambda x: np.log(x - offset)
if caller == "scale":
if self._scale_var.get() == 'linear':
self._saved_log_scale = self._disp.ax.get_ylim()
self._disp.ax.set_ylim(self._saved_lin_scale)
else:
self._saved_lin_scale = self._disp.ax.get_ylim()
self._disp.ax.set_ylim(self._saved_log_scale)
except ValueError:
pass
return _set_scale_inner
[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=[[]]):
"""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 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):
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.extra_widgets_side = tk.LEFT
self.toolbar.pack(side=tk.TOP, fill=tk.BOTH, expand=1)
self.extra_widgets = []
self.extra_widgets_frames = []
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)
#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()
def _init_app_old(self):
self.figure = mpl.figure.Figure()
self.ax = self.figure.add_subplot(111)
self.canvas = FigureCanvasTkAgg(self.figure, self.window)
self.toolbar = CustomToolbar(self.canvas, self.window, 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.canvas.show()
[docs] def init_fig(self, ntracks, npoints):
"""Initialise figure with n tracksof 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
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()