Source code for display

"""
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


[docs]class CustomToolbar(NavigationToolbar2TkAgg): """matplotlib toolbar with extra buttons for: 1. toggle live display 2. That's all! """ # Just add a tuple (Name, Tooltip, Icon, Method) to toolitems to set more buttons. def __init__(self, canvas_, parent_, toggle): self.toolitems = super(CustomToolbar, self).toolitems + (('Live', 'Toggle live display', 'stock_refresh', 'toggle'),) self.toggle = toggle super(CustomToolbar, self).__init__(canvas_, parent_)
[docs]def status_widget(status_var=None, **kwargs): """Return a function that can be passed to :class:`LivePlot` to add an :class:`StatusWidget`""" def inner(parent, display): # Single line widget, return as list return StatusWidget(parent, status_var=status_var, **kwargs) return inner
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)
[docs]def exposure_widget(cam, **kwargs): """Return a function that can be passed to :class:`LivePlot` to add an :class:`ExposureWidget`""" def inner(parent, display): return ExposureWidget(parent, cam, **kwargs) return inner
[docs]class ExposureWidget(tk.Frame): """A Frame widget with an Entry that sets the exposure of the camera.""" def __init__(self, master, cam, top=True): """ :param master: the widget's parent :param cam: the camera (any object with a settable ``exposure`` property) """ self._cam = cam side = tk.TOP if top else tk.LEFT self.top = top tk.Frame.__init__(self, master) self._var = tk.IntVar(self) #self._var.trace('w', self._set_exposure) self._var.set(int(cam.exposure)) self._label = tk.Label(self, text='Exposure (ms)') self._entry = tk.Entry(self, textvariable=self._var, width=5, justify=tk.RIGHT) self._entry.bind('<KeyPress-Return>', self._set_exposure) self._entry.bind('<KeyPress-KP_Enter>', self._set_exposure) self._label.pack(side=side) self._entry.pack() def _set_exposure(self, *args): try: exp = self._var.get() self._cam.exposure = exp except ValueError: pass
[docs]def autorefresh_widget(**kwargs): """Return a function that can be passed to :class:`LivePlot` to add an :class:`AutoRefreshWidgetWidget`.""" def inner(parent, display): return AutoRefreshWidget(parent, display, **kwargs) return inner
[docs]class AutoRefreshWidget(tk.Frame): """A Frame widget with an Button that toggle the display autorefresh state.""" def __init__(self, master, display, top=True): """ :param master: the widget's parent :param disp: the camera display (any object with a settable ``autorefresh`` property) """ self._display = display side = tk.TOP if top else tk.LEFT self.top = top tk.Frame.__init__(self, master) self._autorefresh_status = display.autorefresh self._button = tk.Button(self, width=15, justify=tk.RIGHT, command=self._set_autorefresh) if self._autorefresh_status: self._button.config(text='Autorefresh ON', relief=tk.SUNKEN) else: self._button.config(text='Autorefresh OFF', relief=tk.RAISED) self._button.pack() def _set_autorefresh(self, *args): # Toggle auto-refresh state self._autorefresh_status = not self._display.autorefresh self._display.autorefresh = self._autorefresh_status if self._autorefresh_status: self._button.config(text='Autorefresh ON', relief=tk.SUNKEN) else: self._button.config(text='Autorefresh OFF', relief=tk.RAISED)
[docs]def scale_widget(**kwargs): """Return a function that can be passed to :class:`LivePlot` to add an :class:`VerticalScaleWidget`""" def inner(parent, display): return VerticalScaleWidget(parent, display, **kwargs) return inner
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()