"""
=======
Plotter
=======
.. moduleauthor:: Adam Ginsburg <adam.g.ginsburg@gmail.com>
"""
from __future__ import print_function
import matplotlib
import matplotlib.figure
import numpy as np
import astropy.units as u
import copy
import inspect
from astropy import log
from . import widgets
from ..specwarnings import warn
interactive_help_message = """
Interactive key commands for plotter. An additional help message may appear if
you have initiated the fitter.
'?' - bring up this message
'f' - initiate the /f/itter
'b' - initiate the /b/aseliner
'B' - initiate the /b/aseliner (reset the selection too)
'r' - re-attach matplotlib keys
'R' - redraw the plot cleanly
'i' : individual components / show each fitted component
"""
xlabel_table = {'speed': 'Velocity'}
[docs]
class Plotter(object):
"""
Class to plot a spectrum
"""
def __init__(self, Spectrum, autorefresh=True, title="", xlabel=None,
silent=True, plotscale=1.0, **kwargs):
import matplotlib.pyplot
self._pyplot = matplotlib.pyplot
self.figure = None
self.axis = None
self.Spectrum = Spectrum
# plot parameters
self.offset = 0.0 # vertical offset
self.autorefresh = autorefresh
self.xlabel = xlabel
self.title = title
self.errorplot = None
self.plotkwargs = kwargs
self._xlim = [None,None]
self._ylim = [None,None]
self.debug = False
self.keyclick = None
self.silent = silent
self.plotscale = plotscale
self._xclick1 = None
self._xclick2 = None
self.automake_fitter_tool = False
self._active_gui = None
@property
def _xunit(self):
return self.Spectrum.xarr.unit
def _get_prop(xy, minmax):
def getprop(self):
if xy == 'x':
if minmax == 'min':
if self._xlim[0] is not None and self._xunit:
try:
self._xlim[0]._unit = self._xunit
except AttributeError:
self._xlim[0] = u.Quantity(self._xlim[0], self._xunit)
return self._xlim[0]
elif minmax == 'max':
if self._xlim[1] is not None and self._xunit:
try:
self._xlim[1]._unit = self._xunit
except AttributeError:
self._xlim[1] = u.Quantity(self._xlim[1], self._xunit)
return self._xlim[1]
elif xy == 'y':
if minmax == 'min':
return self._ylim[0]
elif minmax == 'max':
return self._ylim[1]
return getprop
def _set_prop(xy, minmax):
def setprop(self, value):
if self.debug:
frm = inspect.stack()
print(frm[1],"Setting %s%s to %s" % (xy,minmax,value))
if xy == 'x':
if minmax == 'min':
self._xlim[0] = value
elif minmax == 'max':
self._xlim[1] = value
elif xy == 'y':
if minmax == 'min':
self._ylim[0] = value
elif minmax == 'max':
self._ylim[1] = value
return setprop
xmin = property(fget=_get_prop('x','min'),fset=_set_prop('x','min'))
xmax = property(fget=_get_prop('x','max'),fset=_set_prop('x','max'))
ymin = property(fget=_get_prop('y','min'),fset=_set_prop('y','min'))
ymax = property(fget=_get_prop('y','max'),fset=_set_prop('y','max'))
def _disconnect_matplotlib_keys(self):
"""
Disconnect matplotlib's built-in key-press handler (the one that
implements the default 'k', 'l', 'g', 's', ... shortcuts) so it does
not conflict with pyspeckit's interactive keys while an interactive
session is active. The handler is restored by
`_reconnect_matplotlib_keys`.
"""
if self.figure is None:
return
canvas = self.figure.canvas
manager = getattr(canvas, 'manager', None)
handler_id = getattr(manager, 'key_press_handler_id', None)
if handler_id is not None:
# mpl_disconnect is a no-op if the callback id is stale, so this
# is safe even if something else already disconnected it
canvas.mpl_disconnect(handler_id)
manager.key_press_handler_id = None
self._mpl_keys_disconnected = True
def _reconnect_matplotlib_keys(self):
"""
Reconnect the default matplotlib key-press handler that was
disconnected by `_disconnect_matplotlib_keys`.
"""
if self.figure is None or not getattr(self, '_mpl_keys_disconnected',
False):
return
canvas = self.figure.canvas
manager = getattr(canvas, 'manager', None)
if manager is None:
return
if manager.key_press_handler_id is None:
if hasattr(manager, 'key_press'):
# matplotlib < 3.8: the default handler is the manager's
# (bound) key_press method
callback = manager.key_press
else:
# matplotlib >= 3.8 removed FigureManagerBase.key_press; the
# default handler is the key_press_handler function, which
# pulls the canvas from the event
from matplotlib.backend_bases import key_press_handler
callback = key_press_handler
manager.key_press_handler_id = canvas.mpl_connect(
'key_press_event', callback)
self._mpl_keys_disconnected = False
def __call__(self, figure=None, axis=None, clear=True, autorefresh=None,
plotscale=1.0, override_plotkwargs=False, **kwargs):
"""
Plot a spectrum
Keywords:
figure - either a matplotlib figure instance or a figure number
to pass into pyplot.figure.
axis - Alternative to figure, can pass an axis instance and use
it as the plotting canvas
clear - Clear the axis before plotting?
"""
# figure out where to put the plot
if isinstance(figure,matplotlib.figure.Figure):
self.figure = figure
self.axis = self.figure.gca()
elif type(figure) is int:
self.figure = self._pyplot.figure(figure)
self.axis = self.figure.gca()
elif self.figure is None:
if isinstance(axis,matplotlib.axes.Axes):
self.axis = axis
self.figure = axis.figure
else:
self.figure = self._pyplot.figure()
if hasattr(self.figure, 'number') and not self._pyplot.fignum_exists(self.figure.number):
self.figure = self._pyplot.figure(self.figure.number)
# always re-connect the interactive keys to avoid frustration...
self._mpl_reconnect()
if axis is not None:
#self._mpl_disconnect()
self.axis = axis
self.figure = axis.figure
#self._mpl_connect()
elif len(self.figure.axes) > 0 and self.axis is None:
self.axis = self.figure.axes[0] # default to first axis
elif self.axis is None:
self.axis = self.figure.gca()
# A check to deal with issue #117: if you close the figure, the axis
# still exists, but it cannot be reattached to a figure
if (hasattr(self.axis.get_figure(), 'number') and
not (self.axis.get_figure() is self._pyplot.figure(self.axis.get_figure().number))):
self.axis = self.figure.gca()
if self.axis is not None and self.axis not in self.figure.axes:
# if you've cleared the axis, but the figure is still open, you
# need a new axis
self.figure.add_axes(self.axis)
if clear and self.axis is not None:
self.axis.clear()
# Need to empty the stored model plots
if hasattr(self.Spectrum, 'fitter'):
self.Spectrum.fitter.clear()
if autorefresh is not None:
self.autorefresh = autorefresh
self.plotscale = plotscale
if self.plotkwargs and not override_plotkwargs:
self.plotkwargs.update(kwargs)
else:
self.plotkwargs = kwargs
self.plot(**kwargs)
def _mpl_connect(self):
if self.keyclick is None:
self.keyclick = self.figure.canvas.mpl_connect('key_press_event',self.parse_keys)
def _mpl_disconnect(self):
self.figure.canvas.mpl_disconnect(self.keyclick)
self.keyclick = None
[docs]
def disconnect(self):
"""
Disconnect the matplotlib interactivity of this pyspeckit plotter.
"""
self._mpl_disconnect()
[docs]
def connect(self):
"""
Connect to the matplotlib key-parsing interactivity
"""
self._mpl_connect()
def _mpl_reconnect(self):
self._mpl_disconnect()
self._mpl_connect()
# disable fullscreen & grid
self._pyplot.rcParams['keymap.fullscreen'] = 'ctrl+f'
self._pyplot.rcParams['keymap.grid'] = 'ctrl+g'
[docs]
def plot(self, offset=0.0, xoffset=0.0, color='k', drawstyle='steps-mid',
linewidth=0.5, errstyle=None, erralpha=0.2, errcolor=None,
silent=None, reset=True, refresh=True, use_window_limits=None,
useOffset=False, **kwargs):
"""
Plot the spectrum!
Tries to automatically find a reasonable plotting range if one is not
set.
Parameters
----------
offset : float
vertical offset to add to the spectrum before plotting. Useful if
you want to overlay multiple spectra on a single plot
xoffset: float
An x-axis shift. I don't know why you'd want this...
color : str
default to plotting spectrum in black
drawstyle : 'steps-mid' or str
'steps-mid' for histogram-style plotting. See matplotlib's plot
for more information
linewidth : float
Line width in pixels. Narrow lines are helpful when histo-plotting
errstyle : 'fill', 'bars', or None
can be "fill", which draws partially transparent boxes around the
data to show the error region, or "bars" which draws standard
errorbars. ``None`` will display no errorbars
useOffset : bool
Use offset-style X/Y coordinates (e.g., 1 + 1.483e10)? Defaults to
False because these are usually quite annoying.
xmin/xmax/ymin/ymax : float
override defaults for plot range. Once set, these parameters are
sticky (i.e., replotting will use the same ranges). Passed to
`reset_limits`
reset_[xy]limits : bool
Reset the limits to "sensible defaults". Passed to `reset_limits`
ypeakscale : float
Scale up the Y maximum value. Useful to keep the annotations away
from the data. Passed to `reset_limits`
reset : bool
Reset the x/y axis limits? If set, `reset_limits` will be called.
"""
if self.axis is None:
raise Exception("You must call the Plotter class to initiate the canvas before plotting.")
self.offset = offset
# there is a bug where this only seems to update the second time it is called
self.label(**kwargs)
self.label(**kwargs)
for arg in ['title','xlabel','ylabel']:
if arg in kwargs:
kwargs.pop(arg)
reset_kwargs = {}
for arg in ['xmin', 'xmax', 'ymin', 'ymax', 'reset_xlimits',
'reset_ylimits', 'ypeakscale']:
if arg in kwargs:
reset_kwargs[arg] = kwargs.pop(arg)
if (use_window_limits is None and any(k in reset_kwargs for k in
('xmin','xmax','reset_xlimits'))):
use_window_limits = False
if use_window_limits:
self._stash_window_limits()
# for filled errorbars, order matters.
inds = np.argsort(self.Spectrum.xarr)
if errstyle is not None:
if errcolor is None:
errcolor = color
if errstyle == 'fill':
self.errorplot = [self.axis.fill_between(steppify(self.Spectrum.xarr.value[inds]+xoffset, isX=True),
steppify((self.Spectrum.data*self.plotscale+self.offset-self.Spectrum.error*self.plotscale)[inds]),
steppify((self.Spectrum.data*self.plotscale+self.offset+self.Spectrum.error*self.plotscale)[inds]),
facecolor=errcolor, edgecolor=errcolor, alpha=erralpha, **kwargs)]
elif errstyle == 'bars':
self.errorplot = self.axis.errorbar(self.Spectrum.xarr[inds].value+xoffset,
self.Spectrum.data[inds]*self.plotscale+self.offset,
yerr=self.Spectrum.error[inds]*self.plotscale,
ecolor=errcolor, fmt='none',
**kwargs)
self._spectrumplot = self.axis.plot(self.Spectrum.xarr.value[inds]+xoffset,
self.Spectrum.data[inds]*self.plotscale+self.offset,
color=color,
drawstyle=drawstyle,
linewidth=linewidth, **kwargs)
self.axis.ticklabel_format(useOffset=useOffset)
if use_window_limits:
self._reset_to_stashed_limits()
if silent is not None:
self.silent = silent
if reset:
self.reset_limits(use_window_limits=use_window_limits, **reset_kwargs)
if self.autorefresh and refresh:
self.refresh()
# Maybe it's OK to call 'plot' when there is an active gui tool
# (e.g., baseline or specfit)?
#if self._active_gui:
# self._active_gui = None
# warn("An active GUI was found while initializing the "
# "plot. This is somewhat dangerous and may result "
# "in broken interactivity.")
def _stash_window_limits(self):
self._window_limits = self.axis.get_xlim(),self.axis.get_ylim()
if self.debug:
print("Stashed window limits: ",self._window_limits)
def _reset_to_stashed_limits(self):
self.axis.set_xlim(*self._window_limits[0])
self.axis.set_ylim(*self._window_limits[1])
self.xmin,self.xmax = self._window_limits[0]
self.ymin,self.ymax = self._window_limits[1]
if self.debug:
print("Recovered window limits: ",self._window_limits)
[docs]
def reset_limits(self, xmin=None, xmax=None, ymin=None, ymax=None,
reset_xlimits=True, reset_ylimits=True, ypeakscale=1.2,
silent=None, use_window_limits=False, **kwargs):
"""
Automatically or manually reset the plot limits
"""
# if not use_window_limits: use_window_limits = False
if self.debug:
frame = inspect.currentframe()
args, _, _, values = inspect.getargvalues(frame)
print(zip(args,values))
if use_window_limits:
# this means DO NOT reset!
# it simply sets self.[xy][min/max] = current value
self.set_limits_from_visible_window()
else:
if silent is not None:
self.silent = silent
# if self.xmin and self.xmax:
# self.xmin/self.xmax may be None or Quantity; compare plain values
xminval = getattr(self.xmin, 'value', self.xmin)
xmaxval = getattr(self.xmax, 'value', self.xmax)
if (reset_xlimits or xminval is None or xmaxval is None or
self.Spectrum.xarr.min().value < xminval or
self.Spectrum.xarr.max().value > xmaxval):
if not self.silent:
warn("Resetting X-axis min/max because the plot is out of bounds.")
self.xmin = None
self.xmax = None
if xmin is not None:
self.xmin = u.Quantity(xmin, self._xunit)
elif self.xmin is None:
self.xmin = u.Quantity(self.Spectrum.xarr.min().value, self._xunit)
if xmax is not None:
self.xmax = u.Quantity(xmax, self._xunit)
elif self.xmax is None:
self.xmax = u.Quantity(self.Spectrum.xarr.max().value, self._xunit)
xpixmin = np.argmin(np.abs(self.Spectrum.xarr.value-self.xmin.value))
xpixmax = np.argmin(np.abs(self.Spectrum.xarr.value-self.xmax.value))
if xpixmin>xpixmax:
xpixmin,xpixmax = xpixmax,xpixmin
elif xpixmin == xpixmax:
if reset_xlimits:
raise Exception("Infinite recursion error. Maybe there are no valid data?")
if not self.silent:
warn("ERROR: the X axis limits specified were invalid. Resetting.")
self.reset_limits(reset_xlimits=True, ymin=ymin, ymax=ymax,
reset_ylimits=reset_ylimits,
ypeakscale=ypeakscale, **kwargs)
return
if self.ymin is not None and self.ymax is not None:
# this is utter nonsense....
if (np.nanmax(self.Spectrum.data) < self.ymin or np.nanmin(self.Spectrum.data) > self.ymax
or reset_ylimits):
if not self.silent and not reset_ylimits:
warn("Resetting Y-axis min/max because the plot is out of bounds.")
self.ymin = None
self.ymax = None
if ymin is not None:
self.ymin = ymin
elif self.ymin is None:
yminval = np.nanmin(self.Spectrum.data[xpixmin:xpixmax])
# Increase the range fractionally. This means dividing a positive #, multiplying a negative #
if yminval < 0:
self.ymin = float(yminval)*float(ypeakscale)
else:
self.ymin = float(yminval)/float(ypeakscale)
if ymax is not None:
self.ymax = ymax
elif self.ymax is None:
ymaxval = (np.nanmax(self.Spectrum.data[xpixmin:xpixmax])-self.ymin)
if ymaxval > 0:
self.ymax = float(ymaxval) * float(ypeakscale) + self.ymin
else:
self.ymax = float(ymaxval) / float(ypeakscale) + self.ymin
self.ymin += self.offset
self.ymax += self.offset
self.axis.set_xlim(self.xmin.value if hasattr(self.xmin, 'value') else self.xmin,
self.xmax.value if hasattr(self.xmax, 'value') else self.xmax)
self.axis.set_ylim(self.ymin, self.ymax)
[docs]
def label(self, title=None, xlabel=None, ylabel=None, verbose_label=False,
**kwargs):
"""
Label the plot, with an attempt to parse standard units into nice latex labels
Parameters
----------
title : str
xlabel : str
ylabel : str
verbose_label: bool
"""
if title is not None:
self.title = title
elif hasattr(self.Spectrum,'specname'):
self.title = self.Spectrum.specname
if self.title != "":
self.axis.set_title(self.title)
if xlabel is not None:
log.debug("setting xlabel={0}".format(xlabel))
self.xlabel = xlabel
elif self._xunit:
try:
self.xlabel = xlabel_table[str(self._xunit.physical_type).lower()]
except KeyError:
self.xlabel = str(self._xunit.physical_type)
# WAS: self.xlabel += " ("+u.Unit(self._xunit).to_string()+")"
self.xlabel += " ({0})".format(self._xunit.to_string())
log.debug("xunit is {1}. set xlabel={0}".format(self.xlabel,
self._xunit))
if verbose_label:
self.xlabel = "%s %s" % (str(self.Spectrum.xarr.velocity_convention),
self.xlabel)
else:
log.warning("Plotter: xlabel was not set")
if self.xlabel is not None:
self.axis.set_xlabel(self.xlabel)
if ylabel is not None:
self.axis.set_ylabel(ylabel)
elif self.Spectrum.unit in ['Ta*','Tastar']:
self.axis.set_ylabel("$T_A^*$ (K)")
elif self.Spectrum.unit in ['K']:
self.axis.set_ylabel("Brightness Temperature $T$ (K)")
elif self.Spectrum.unit == 'mJy':
self.axis.set_ylabel("$S_\\nu$ (mJy)")
elif self.Spectrum.unit == 'Jy':
self.axis.set_ylabel("$S_\\nu$ (Jy)")
else:
if isinstance(self.Spectrum.unit, str) and "$" in self.Spectrum.unit:
# assume LaTeX already
self.axis.set_ylabel(self.Spectrum.unit)
elif isinstance(self.Spectrum.unit, str):
self.axis.set_ylabel(self.Spectrum.unit)
else:
label_units = self.Spectrum.unit.to_string(format='latex')
if 'mathring{A}' in label_units:
label_units = label_units.replace('\\mathring{A}', 'A')
if '\\overset' in label_units:
label_units = label_units.replace('\\overset', '^')
self.axis.set_ylabel(label_units)
@property
def ylabel(self):
return self.axis.get_ylabel()
def refresh(self):
if self.axis is not None:
self.axis.figure.canvas.draw()
[docs]
def savefig(self,fname,bbox_inches='tight',**kwargs):
"""
simple wrapper of maplotlib's savefig.
"""
self.axis.figure.savefig(fname,bbox_inches=bbox_inches,**kwargs)
[docs]
def parse_keys(self,event):
"""
Parse key commands entered from the keyboard
"""
if hasattr(event,'key'):
if event.key == '?':
print(interactive_help_message)
elif event.key == 'f':
print("\n\nFitter initiated from the interactive plotter.")
# extra optional text:
# Matplotlib shortcut keys ('g','l','p',etc.) are disabled. Re-enable with 'r'"
if self._active_gui == self.Spectrum.specfit and self._active_gui._check_connections(verbose=False):
print("Fitter is already active. Use 'q' to quit the fitter.")
elif self._active_gui == self.Spectrum.specfit and not self._active_gui._check_connections(verbose=False):
# forcibly clear connections
self._active_gui.clear_all_connections()
# the 'clear_all_connections' code *explicitly* makes the
# following line correct, except in the case that there is
# no canvas...
assert self._active_gui is None
self.activate_interactive_fitter()
else:
self.activate_interactive_fitter()
assert self._active_gui == self.Spectrum.specfit
assert self._active_gui._check_connections(verbose=False)
if not hasattr(self,'FitterTool') and self.automake_fitter_tool:
self.FitterTool = widgets.FitterTools(self.Spectrum.specfit, self.figure)
elif hasattr(self,'FitterTool') and self.FitterTool.toolfig.number not in self._pyplot.get_fignums():
self.FitterTool = widgets.FitterTools(self.Spectrum.specfit, self.figure)
elif event.key is not None and event.key.lower() == 'b':
if event.key == 'b':
print("\n\nBaseline initiated from the interactive plotter")
elif event.key == 'B':
print("\n\nBaseline initiated from the interactive plotter (with reset)")
print("Matplotlib shortcut keys ('g','l','p',etc.) are disabled. Re-enable with 'r'")
self.activate_interactive_baseline_fitter(reset_selection=(event.key=='B'))
if not hasattr(self,'FitterTool') and self.automake_fitter_tool:
self.FitterTool = widgets.FitterTools(self.Spectrum.specfit, self.figure)
elif hasattr(self,'FitterTool') and self.FitterTool.toolfig.number not in self._pyplot.get_fignums():
self.FitterTool = widgets.FitterTools(self.Spectrum.specfit, self.figure)
elif event.key == 'r':
# print("\n\nReconnected matplotlib shortcut keys.")
self._reconnect_matplotlib_keys()
elif event.key == 'R':
self()
elif event.key == 'i':
self.Spectrum.specfit.plot_fit(show_components=True)
def get_two_clicks(self,event):
if self._xclick1 is None:
self._xclick1 = event.xdata
elif self._xclick2 is None:
self._xclick2 = event.xdata
[docs]
def set_limits_from_visible_window(self, debug=False):
""" Hopefully self-descriptive: set the x and y limits from the
currently visible window (use this if you use the pan/zoom tools or
manually change the limits) """
if debug:
print("Changing x limits from {},{} to {},{}".format(self.xmin,self.xmax,self.axis.get_xlim()[0],self.axis.get_xlim()[1]))
print("Changing y limits from {},{} to {},{}".format(self.ymin,self.ymax,self.axis.get_ylim()[0],self.axis.get_ylim()[1]))
self.xmin, self.xmax = self.axis.get_xlim()
self.ymin, self.ymax = self.axis.get_ylim()
if debug:
print("New x limits {},{} == {},{}".format(self.xmin,self.xmax,self.axis.get_xlim()[0],self.axis.get_xlim()[1]))
print("New y limits {},{} == {},{}".format(self.ymin,self.ymax,self.axis.get_ylim()[0],self.axis.get_ylim()[1]))
[docs]
def copy(self, parent=None):
"""
Create a copy of the plotter with blank (uninitialized) axis & figure
[ parent ]
A spectroscopic axis instance that is the parent of the specfit
instance. This needs to be specified at some point, but defaults
to None to prevent overwriting a previous plot.
"""
newplotter = copy.copy(self)
newplotter.Spectrum = parent
newplotter.axis = None
newplotter.figure = None
return newplotter
[docs]
def line_ids(self, line_names, line_xvals, xval_units=None, auto_yloc=True,
velocity_offset=None, velocity_convention='radio',
auto_yloc_fraction=0.9, **kwargs):
"""
Add line ID labels to a plot using lineid_plot
http://oneau.wordpress.com/2011/10/01/line-id-plot/
https://github.com/phn/lineid_plot
http://packages.python.org/lineid_plot/
Parameters
----------
line_names : list
A list of strings to label the specified x-axis values
line_xvals : list
List of x-axis values (e.g., wavelengths) at which to label the lines.
Can be a list of quantities.
xval_units : string
The unit of the line_xvals if they are not given as quantities
velocity_offset : quantity
A velocity offset to apply to the inputs if they are in frequency
or wavelength units
velocity_convention : 'radio' or 'optical' or 'doppler'
Used if the velocity offset is given
auto_yloc : bool
If set, overrides box_loc and arrow_tip (the vertical position of
the lineid labels) in kwargs to be `auto_yloc_fraction` of the plot
range
auto_yloc_fraction: float in range [0,1]
The fraction of the plot (vertically) at which to place labels
Examples
--------
>>> import numpy as np
>>> import pyspeckit
>>> sp = pyspeckit.Spectrum(
xarr=pyspeckit.units.SpectroscopicAxis(np.linspace(-50,50,101),
unit='km/s', refX=6562.8, refX_unit='angstrom'),
data=np.random.randn(101), error=np.ones(101))
>>> sp.plotter()
>>> sp.plotter.line_ids(['H$\\alpha$'],[6562.8],xval_units='angstrom')
"""
import lineid_plot
if velocity_offset is not None:
assert velocity_offset.unit.is_equivalent(u.km/u.s)
doppler = getattr(u, 'doppler_{0}'.format(velocity_convention))
if self.Spectrum.xarr.refX is not None:
equivalency = doppler(self.Spectrum.xarr.refX)
else:
equivalency = doppler(self.Spectrum.xarr.as_unit(u.GHz)[0])
xvals = []
linenames_toplot = []
for xv,ln in zip(line_xvals, line_names):
if hasattr(xv, 'unit'):
pass
else:
xv = u.Quantity(xv, xval_units)
xv = xv.to(u.km/u.s,
equivalencies=equivalency)
if velocity_offset is not None:
xv = xv + velocity_offset
xv = xv.to(self.Spectrum.xarr.unit, equivalencies=equivalency)
if self.Spectrum.xarr.in_range(xv):
xvals.append(xv.value)
linenames_toplot.append(ln)
if len(xvals) != len(line_xvals):
log.warning("Skipped {0} out-of-bounds lines when plotting line IDs."
.format(len(line_xvals)-len(xvals)))
if auto_yloc:
yr = self.axis.get_ylim()
kwargs['box_loc'] = (yr[1]-yr[0])*auto_yloc_fraction + yr[0]
kwargs['arrow_tip'] = (yr[1]-yr[0])*(auto_yloc_fraction*0.9) + yr[0]
lineid_plot.plot_line_ids(self.Spectrum.xarr,
self.Spectrum.data,
xvals,
linenames_toplot,
ax=self.axis,
**kwargs)
[docs]
def line_ids_from_measurements(self, auto_yloc=True,
auto_yloc_fraction=0.9, **kwargs):
"""
Add line ID labels to a plot using lineid_plot
http://oneau.wordpress.com/2011/10/01/line-id-plot/
https://github.com/phn/lineid_plot
http://packages.python.org/lineid_plot/
Parameters
----------
auto_yloc : bool
If set, overrides box_loc and arrow_tip (the vertical position of
the lineid labels) in kwargs to be `auto_yloc_fraction` of the plot
range
auto_yloc_fraction: float in range [0,1]
The fraction of the plot (vertically) at which to place labels
Examples
--------
>>> import numpy as np
>>> import pyspeckit
>>> sp = pyspeckit.Spectrum(
xarr=pyspeckit.units.SpectroscopicAxis(np.linspace(-50,50,101),
units='km/s', refX=6562.8, refX_unit='angstroms'),
data=np.random.randn(101), error=np.ones(101))
>>> sp.plotter()
>>> sp.specfit(multifit=None, fittype='gaussian', guesses=[1,0,1]) # fitting noise....
>>> sp.measure()
>>> sp.plotter.line_ids_from_measurements()
"""
import lineid_plot
if hasattr(self.Spectrum,'measurements'):
measurements = self.Spectrum.measurements
if auto_yloc:
yr = self.axis.get_ylim()
kwargs['box_loc'] = (yr[1]-yr[0])*auto_yloc_fraction + yr[0]
kwargs['arrow_tip'] = (yr[1]-yr[0])*(auto_yloc_fraction*0.9) + yr[0]
lineid_plot.plot_line_ids(self.Spectrum.xarr, self.Spectrum.data,
[v['pos'] for v in
measurements.lines.values()],
measurements.lines.keys(), ax=self.axis,
**kwargs)
else:
warn("Cannot add line IDs from measurements unless measurements have been made!")
[docs]
def activate_interactive_fitter(self):
"""
Attempt to activate the interactive fitter
"""
if self._active_gui is not None:
# This should not be reachable. Clearing connections is the
# "right" behavior if this becomes reachable, but I'd rather raise
# an exception because I don't want to get here ever
self._active_gui.clear_all_connections()
raise ValueError("GUI was active when 'f' key pressed")
self._activate_interactive(self.Spectrum.specfit, interactive=True)
[docs]
def activate_interactive_baseline_fitter(self, **kwargs):
"""
Attempt to activate the interactive baseline fitter
"""
if self._active_gui is not None:
# This should not be reachable. Clearing connections is the
# "right" behavior if this becomes reachable, but I'd rather raise
# an exception because I don't want to get here ever
gui_was = self._active_gui
self._active_gui.clear_all_connections()
raise ValueError("GUI {0} was active when 'b' key pressed"
.format(gui_was))
self._activate_interactive(self.Spectrum.baseline, interactive=True,
**kwargs)
def _activate_interactive(self, object_to_activate, **kwargs):
self._disconnect_matplotlib_keys()
self._active_gui = object_to_activate
# activating the gui calls clear_all_connections, which disconnects the
# gui
try:
self._active_gui(**kwargs)
self._active_gui = object_to_activate
assert self._active_gui is not None
except Exception as ex:
self._active_gui = None
raise ex
def parse_units(labelstring):
import re
labelstring = re.sub("um","$\\mu$m",labelstring)
labelstring = re.sub("-1","$^{-1}$",labelstring)
labelstring = re.sub("-2","$^{-2}$",labelstring)
labelstring = re.sub("-3","$^{-3}$",labelstring)
labelstring = re.sub("ergss","ergs s",labelstring)
return labelstring
[docs]
def parse_norm(norm):
"""
Expected format: norm = 10E15
"""
try:
base, exp = norm.split('E')
except ValueError:
base, exp = norm.split('e')
if float(base) == 1.0:
norm = '10'
else:
norm = base
norm += '^{%s}' % exp
return norm
[docs]
def steppify(arr,isX=False):
"""
*support function*
Converts an array to double-length for step plotting
"""
if isX:
interval = abs(arr[1:]-arr[:-1]) / 2.0
newarr = np.array(list(zip(arr[:-1]-interval,arr[:-1]+interval))).ravel()
newarr = np.concatenate([newarr,2*[newarr[-1]+interval[-1]]])
else:
newarr = np.array(list(zip(arr,arr))).ravel()
return newarr