Source code for aplpy.core
from distutils import version
import os
import operator
from functools import reduce
import matplotlib
import matplotlib.pyplot as plt
from matplotlib.patches import Circle, Rectangle, Ellipse, Polygon, FancyArrow
from matplotlib.collections import PatchCollection, LineCollection
import numpy as np
from astropy import log
from astropy.wcs import WCS
from astropy.wcs.utils import proj_plane_pixel_scales
from astropy.io import fits
from astropy.nddata.utils import block_reduce
from astropy.visualization import AsymmetricPercentileInterval
from astropy.visualization.wcsaxes import WCSAxes, WCSAxesSubplot
from astropy.coordinates import ICRS
from . import convolve_util
from . import header as header_util
from . import slicer
from .compat import simple_norm
from .layers import Layers
from .grid import Grid
from .ticks import Ticks
from .tick_labels import TickLabels
from .axis_labels import AxisLabels
from .overlays import Beam, Scalebar
from .regions import Regions
from .colorbar import Colorbar
from .frame import Frame
from .decorators import auto_refresh, fixdocstring
HDU_TYPES = tuple([fits.PrimaryHDU, fits.ImageHDU, fits.CompImageHDU])
__doctest_skip__ = ['FITSFigure.add_beam', 'FITSFigure.add_colorbar',
'FITSFigure.add_grid', 'FITSFigure.add_scalebar']
def uniformize_1d(*args):
if len(args) > 1:
return np.broadcast_arrays(np.atleast_1d(args[0]), *args[1:])
elif len(args) == 1:
return np.atleast_1d(args[0])
else:
raise ValueError("No arguments passed to uniformize_1d")
[docs]class FITSFigure(Layers, Regions):
"""
Create a FITSFigure instance.
This class is a wrapper around the Astropy WCSAxes class and provides
the same API as historical versions of APLpy.
Parameters
----------
data : see below
The FITS file to open. The following data types can be passed:
string
astropy.io.fits.PrimaryHDU
astropy.io.fits.ImageHDU
astropy.wcs.WCS
np.ndarray
RGB image with AVM meta-data
hdu : int, optional
By default, the image in the primary HDU is read in. If a
different HDU is required, use this argument.
figure : ~matplotlib.figure.Figure, optional
If specified, a subplot will be added to this existing
matplotlib figure() instance, rather than a new figure
being created from scratch.
subplot : tuple or list, optional
If specified, a subplot will be added at this position. If a tuple
of three values, the tuple should contain the standard matplotlib
subplot parameters, i.e. (ny, nx, subplot). If a list of four
values, the list should contain [xmin, ymin, dx, dy] where xmin
and ymin are the position of the bottom left corner of the
subplot, and dx and dy are the width and height of the subplot
respectively. These should all be given in units of the figure
width and height. For example, [0.1, 0.1, 0.8, 0.8] will almost
fill the entire figure, leaving a 10 percent margin on all sides.
downsample : int, optional
If this option is specified, the image will be downsampled
by a factor *downsample* when reading in the data.
north : bool, optional
Whether to rotate the image so that north is up. By default, this is
assumed to be 'north' in the ICRS frame, but you can also pass any
astropy :class:`~astropy.coordinates.BaseCoordinateFrame` to indicate
to use the north of that frame.
convention : str, optional
This is used in cases where a FITS header can be interpreted
in multiple ways. For example, for files with a -CAR
projection and CRVAL2=0, this can be set to 'wells' or
'calabretta' to choose the appropriate convention.
dimensions : tuple or list, optional
The index of the axes to use if the data has more than three
dimensions.
slices : tuple or list, optional
If a FITS file with more than two dimensions is specified,
then these are the slices to extract. If all extra dimensions
only have size 1, then this is not required.
auto_refresh : bool, optional
Whether to refresh the figure automatically every time a
plotting method is called. This can also be set using the
set_auto_refresh method. This defaults to `True` if and only if
APLpy is being used from IPython and the Matplotlib backend is
interactive.
kwargs
Any additional arguments are passed on to matplotlib's Figure()
class. For example, to set the figure size, use the
figsize=(xsize, ysize) argument (where xsize and ysize are in
inches). For more information on these additional arguments, see
the *Optional keyword arguments* section in the documentation for
:class:`~matplotlib.figure.Figure`.
"""
@auto_refresh
def __init__(self, data, hdu=0, figure=None, subplot=(1, 1, 1),
downsample=False, north=False, convention=None,
dimensions=[0, 1], slices=[], auto_refresh=None,
**kwargs):
self._wcsaxes_slices = ('x', 'y')
if 'figsize' not in kwargs:
kwargs['figsize'] = (10, 9)
# Set the grid type
if len(slices) > 0:
self.grid_type = 'contours'
else:
self.grid_type = 'lines'
if (isinstance(data, str) and
data.split('.')[-1].lower() in ['png', 'jpg', 'tif']):
try:
from PIL import Image
except ImportError:
try:
import Image
except ImportError:
raise ImportError("The Python Imaging Library (PIL) is "
"required to read in RGB images")
try:
import pyavm
except ImportError:
raise ImportError("PyAVM is required to read in AVM "
"meta-data from RGB images")
if version.LooseVersion(pyavm.__version__) < version.LooseVersion('0.9.1'):
raise ImportError("PyAVM installation is not recent enough "
"(version 0.9.1 or later is required).")
from pyavm import AVM
# Remember image filename
self._rgb_image = data
# Find image size
nx, ny = Image.open(data).size
# Now convert AVM information to WCS
data = AVM.from_image(data).to_wcs()
# Need to scale CDELT values sometimes the AVM meta-data is only
# really valid for the full-resolution image
data.wcs.cdelt = [data.wcs.cdelt[0] * nx / float(nx), data.wcs.cdelt[1] * ny / float(ny)]
data.wcs.crpix = [data.wcs.crpix[0] / nx * float(nx), data.wcs.crpix[1] / ny * float(ny)]
# Update the NAXIS values with the true dimensions of the RGB image
data.nx = nx
data.ny = ny
data.pixel_shape = (nx, ny)
if isinstance(data, WCS):
wcs = data
if wcs.naxis != 2:
raise ValueError("FITSFigure initialization via WCS objects "
"can only be done with 2-dimensional WCS "
"objects")
if wcs.pixel_shape is None:
raise ValueError("The WCS object does not contain any size "
"information")
header = wcs.to_header()
header['NAXIS1'], header['NAXIS2'] = wcs.pixel_shape
nx = header['NAXIS%i' % (dimensions[0] + 1)]
ny = header['NAXIS%i' % (dimensions[1] + 1)]
self._data = np.zeros((ny, nx), dtype=float)
self._header = header
self._wcs = WCS(header, relax=True)
self._wcs.nx = nx
self._wcs.ny = ny
if downsample:
log.warning("downsample argument is ignored if data "
"passed is a WCS object")
downsample = False
if north:
log.warning("north argument is ignored if data "
"passed is a WCS object")
north = False
else:
self._data, self._header, self._wcs, self._wcsaxes_slices = \
self._get_hdu(data, hdu, north, convention=convention,
dimensions=dimensions, slices=slices)
self._wcs.nx = self._header['NAXIS%i' % (dimensions[0] + 1)]
self._wcs.ny = self._header['NAXIS%i' % (dimensions[1] + 1)]
# Downsample if requested
if downsample:
nx_new = self._wcs.nx - np.mod(self._wcs.nx, downsample)
ny_new = self._wcs.ny - np.mod(self._wcs.ny, downsample)
self._data = self._data[0:ny_new, 0:nx_new]
self._data = block_reduce(self._data, downsample, func=np.mean)
self._wcs.nx, self._wcs.ny = nx_new, ny_new
# Open the figure
if figure:
self._figure = figure
else:
self._figure = plt.figure(**kwargs)
# Set whether to automatically refresh the display
self.set_auto_refresh(auto_refresh)
# Initialize axis instance
if type(subplot) == list and len(subplot) == 4:
self.ax = WCSAxes(self._figure, subplot, wcs=self._wcs,
slices=self._wcsaxes_slices, adjustable='datalim')
self._figure.add_axes(self.ax)
elif type(subplot) == tuple and len(subplot) == 3:
self.ax = WCSAxesSubplot(self._figure, *subplot, wcs=self._wcs,
slices=self._wcsaxes_slices)
self._figure.add_subplot(self.ax)
else:
raise ValueError("subplot= should be either a tuple of three "
"values, or a list of four values")
# Turn off autoscaling
self.ax.set_autoscale_on(False)
# Make sure axes are above everything else
self.ax.set_axisbelow(False)
# Set view to whole FITS file
self._initialize_view()
# Set the coordinates for x and y axis
self.x = dimensions[0]
self.y = dimensions[1]
# Initialize tick, label, and frame convenience wrappers (these dispatch
# calls to WCSAxes)
self.ticks = Ticks(self)
self.axis_labels = AxisLabels(self)
self.tick_labels = TickLabels(self)
self.frame = Frame(self)
# Display minor ticks
self.ax.coords[self.x].display_minor_ticks(True)
self.ax.coords[self.y].display_minor_ticks(True)
# Initialize layers list
self._initialize_layers()
# Set image holder to be empty
self.image = None
# Set default theme
self.set_theme(theme='pretty')
def _get_hdu(self, data, hdu, north, convention=None,
dimensions=[0, 1], slices=[]):
if isinstance(data, str):
filename = data
# Check file exists
if not os.path.exists(filename):
raise IOError("File not found: " + filename)
# Read in FITS file
try:
hdulist = fits.open(filename)
except Exception:
raise IOError("An error occurred while reading the FITS file")
# Check whether the HDU specified contains any data, otherwise
# cycle through all HDUs to find one that contains valid image data
if hdulist[hdu].data is None:
found = False
for alt_hdu in range(len(hdulist)):
if isinstance(hdulist[alt_hdu], HDU_TYPES):
if hdulist[alt_hdu].data is not None:
log.warning("hdu=%i does not contain any data, "
"using hdu=%i instead" % (hdu, alt_hdu))
hdu = hdulist[alt_hdu]
found = True
break
if not found:
raise Exception("FITS file does not contain any image data")
else:
hdu = hdulist[hdu]
elif type(data) == np.ndarray:
hdu = fits.ImageHDU(data)
elif isinstance(data, HDU_TYPES):
hdu = data
elif isinstance(data, fits.HDUList):
hdu = data[hdu]
else:
raise Exception("data argument should either be a filename, an HDU object from astropy.io.fits, a WCS object from astropy.wcs, or a Numpy array.")
# Check that we have at least 2-dimensional data
if hdu.header['NAXIS'] < 2:
raise ValueError("Data should have at least two dimensions")
# Check dimensions= argument
if type(dimensions) not in [list, tuple]:
raise ValueError('dimensions= should be a list or a tuple')
if len(set(dimensions)) != 2 or len(dimensions) != 2:
raise ValueError("dimensions= should be a tuple of two different values")
if dimensions[0] < 0 or dimensions[0] > hdu.header['NAXIS'] - 1:
raise ValueError('values of dimensions= should be between %i and %i' % (0, hdu.header['NAXIS'] - 1))
if dimensions[1] < 0 or dimensions[1] > hdu.header['NAXIS'] - 1:
raise ValueError('values of dimensions= should be between %i and %i' % (0, hdu.header['NAXIS'] - 1))
# Reproject to face north if requested
if north:
# Find rotated WCS
frame = ICRS() if north is True else north
from reproject.mosaicking import find_optimal_celestial_wcs
wcs, shape = find_optimal_celestial_wcs([hdu], frame=frame)
from reproject import reproject_interp
data, _ = reproject_interp(hdu, wcs, shape_out=shape)
header = wcs.to_header()
header['NAXIS1'] = shape[1]
header['NAXIS2'] = shape[0]
else:
# Now copy the data and header to new objects, since in astropy.io.fits
# the two attributes are linked, which can lead to confusing behavior.
# We just need to copy the header to avoid memory issues - as long as
# one item is copied, the two variables are decoupled.
data = hdu.data
header = hdu.header.copy()
del hdu
# If slices wasn't specified, check if we can guess
shape = data.shape
if len(shape) > 2:
n_total = reduce(operator.mul, shape)
n_image = (shape[len(shape) - 1 - dimensions[0]] *
shape[len(shape) - 1 - dimensions[1]])
if n_total == n_image:
slices = [0 for i in range(1, len(shape) - 1)]
log.info("Setting slices=%s" % str(slices))
# Extract slices
data, wcsaxes_slices = slicer.slice_hypercube(data, header,
dimensions=dimensions,
slices=slices)
# Check header
header = header_util.check(header, convention=convention,
dimensions=dimensions)
# Parse WCS info
wcs = WCS(header, relax=True)
return data, header, wcs, wcsaxes_slices
[docs] @auto_refresh
def set_title(self, title, **kwargs):
"""
Set the figure title
"""
self.ax.set_title(title, **kwargs)
[docs] @auto_refresh
def set_xaxis_coord_type(self, coord_type):
"""
Set the type of x coordinate.
Options are:
* ``scalar``: treat the values are normal decimal scalar values
* ``longitude``: treat the values as a longitude in the 0 to 360 range
* ``latitude``: treat the values as a latitude in the -90 to 90 range
"""
self.ax.coords[self.x].set_coord_type(coord_type)
[docs] @auto_refresh
def set_yaxis_coord_type(self, coord_type):
"""
Set the type of y coordinate.
Options are:
* ``scalar``: treat the values are normal decimal scalar values
* ``longitude``: treat the values as a longitude in the 0 to 360 range
* ``latitude``: treat the values as a latitude in the -90 to 90 range
"""
self.ax.coords[self.y].set_coord_type(coord_type)
[docs] @auto_refresh
def set_system_latex(self, usetex):
"""
Set whether to use a real LaTeX installation or the built-in matplotlib
LaTeX.
Parameters
----------
usetex : str
Whether to use a real LaTex installation (True) or the built-in
matplotlib LaTeX (False). Note that if the former is chosen, an
installation of LaTex is required.
"""
plt.rc('text', usetex=usetex)
[docs] @auto_refresh
def recenter(self, x, y, radius=None, width=None, height=None):
"""
Center the image on a given position and with a given radius.
Either the radius or width/height arguments should be specified. The
units of the radius or width/height should be the same as the world
coordinates in the WCS. For images of the sky, this is often (but not
always) degrees.
Parameters
----------
x, y : float
Coordinates to center on
radius : float, optional
Radius of the region to view in degrees. This produces a square plot.
width : float, optional
Width of the region to view. This should be given in
conjunction with the height argument.
height : float, optional
Height of the region to view. This should be given in
conjunction with the width argument.
"""
xpix, ypix = self.world2pixel(x, y)
pix_scale = proj_plane_pixel_scales(self._wcs)
sx, sy = pix_scale[self.x], pix_scale[self.y]
if radius:
dx_pix = radius / sx
dy_pix = radius / sy
elif width and height:
dx_pix = width / sx * 0.5
dy_pix = height / sy * 0.5
else:
raise Exception("Need to specify either radius= or width= and height= arguments")
if (xpix + dx_pix < -0.5 or
xpix - dx_pix > self._wcs.nx - 0.5 or
ypix + dy_pix < -0.5 or
ypix - dy_pix > self._wcs.ny):
raise Exception("Zoom region falls outside the image")
self.ax.set_xlim(xpix - dx_pix, xpix + dx_pix)
self.ax.set_ylim(ypix - dy_pix, ypix + dy_pix)
[docs] @auto_refresh
def show_grayscale(self, vmin=None, vmid=None, vmax=None,
pmin=0.25, pmax=99.75,
stretch='linear', exponent=2, invert='default',
smooth=None, kernel='gauss', aspect='equal',
interpolation='nearest'):
"""
Show a grayscale image of the FITS file.
Parameters
----------
vmin : None or float, optional
Minimum pixel value to use for the grayscale. If set to None,
the minimum pixel value is determined using pmin (default).
vmax : None or float, optional
Maximum pixel value to use for the grayscale. If set to None,
the maximum pixel value is determined using pmax (default).
pmin : float, optional
Percentile value used to determine the minimum pixel value to
use for the grayscale if vmin is set to None. The default
value is 0.25%.
pmax : float, optional
Percentile value used to determine the maximum pixel value to
use for the grayscale if vmax is set to None. The default
value is 99.75%.
stretch : { 'linear', 'log', 'sqrt', 'arcsinh', 'power' }, optional
The stretch function to use
vmid : None or float, optional
Baseline value used for the log and arcsinh stretches. If
set to None, this is set to zero for log stretches and to
vmin - (vmax - vmin) / 30. for arcsinh stretches
exponent : float, optional
If stretch is set to 'power', this is the exponent to use
invert : str, optional
Whether to invert the grayscale or not. The default is False,
unless set_theme is used, in which case the default depends on
the theme.
smooth : int or tuple, optional
Default smoothing scale is 3 pixels across. User can define
whether they want an NxN kernel (integer), or NxM kernel
(tuple). This argument corresponds to the 'gauss' and 'box'
smoothing kernels.
kernel : { 'gauss', 'box', numpy.array }, optional
Default kernel used for smoothing is 'gauss'. The user can
specify if they would prefer 'gauss', 'box', or a custom
kernel. All kernels are normalized to ensure flux retention.
aspect : { 'auto', 'equal' }, optional
Whether to change the aspect ratio of the image to match that
of the axes ('auto') or to change the aspect ratio of the axes
to match that of the data ('equal'; default)
interpolation : str, optional
The type of interpolation to use for the image. The default is
'nearest'. Other options include 'none' (no interpolation,
meaning that if exported to a postscript file, the grayscale
will be output at native resolution irrespective of the dpi
setting), 'bilinear', 'bicubic', and many more (see the
matplotlib documentation for imshow).
"""
if invert == 'default':
invert = self._get_invert_default()
if invert:
cmap = 'gist_yarg'
else:
cmap = 'gray'
self.show_colorscale(vmin=vmin, vmid=vmid, vmax=vmax,
pmin=pmin, pmax=pmax,
stretch=stretch, exponent=exponent, cmap=cmap,
smooth=smooth, kernel=kernel, aspect=aspect,
interpolation=interpolation)
[docs] @auto_refresh
def hide_grayscale(self, *args, **kwargs):
self.hide_colorscale(*args, **kwargs)
[docs] @auto_refresh
def show_colorscale(self, vmin=None, vmid=None, vmax=None, pmin=0.25,
pmax=99.75, stretch='linear', exponent=2,
cmap='default', smooth=None, kernel='gauss',
aspect='equal', interpolation='nearest'):
"""
Show a colorscale image of the FITS file.
Parameters
----------
vmin : None or float, optional
Minimum pixel value to use for the colorscale. If set to None,
the minimum pixel value is determined using pmin (default).
vmax : None or float, optional
Maximum pixel value to use for the colorscale. If set to None,
the maximum pixel value is determined using pmax (default).
pmin : float, optional
Percentile value used to determine the minimum pixel value to
use for the colorscale if vmin is set to None. The default
value is 0.25%.
pmax : float, optional
Percentile value used to determine the maximum pixel value to
use for the colorscale if vmax is set to None. The default
value is 99.75%.
stretch : { 'linear', 'log', 'sqrt', 'arcsinh', 'power' }, optional
The stretch function to use
vmid : None or float, optional
Baseline value used for the log and arcsinh stretches. If
not set, this defaults to zero for log stretches and to
vmin - (vmax - vmin) / 30. for arcsinh stretches
exponent : float, optional
If stretch is set to 'power', this is the exponent to use
cmap : str, optional
The name of the colormap to use
smooth : int or tuple, optional
Default smoothing scale is 3 pixels across. User can define
whether they want an NxN kernel (integer), or NxM kernel
(tuple). This argument corresponds to the 'gauss' and 'box'
smoothing kernels.
kernel : { 'gauss', 'box', numpy.array }, optional
Default kernel used for smoothing is 'gauss'. The user can
specify if they would prefer 'gauss', 'box', or a custom
kernel. All kernels are normalized to ensure flux retention.
aspect : { 'auto', 'equal' }, optional
Whether to change the aspect ratio of the image to match that
of the axes ('auto') or to change the aspect ratio of the axes
to match that of the data ('equal'; default)
interpolation : str, optional
The type of interpolation to use for the image. The default is
'nearest'. Other options include 'none' (no interpolation,
meaning that if exported to a postscript file, the colorscale
will be output at native resolution irrespective of the dpi
setting), 'bilinear', 'bicubic', and many more (see the
matplotlib documentation for imshow).
"""
if cmap == 'default':
cmap = self._get_colormap_default()
min_auto = vmin is None
max_auto = vmax is None
# The set of available functions
cmap = plt.cm.get_cmap(cmap)
if min_auto or max_auto:
interval = AsymmetricPercentileInterval(pmin, pmax, n_samples=10000)
try:
vmin_auto, vmax_auto = interval.get_limits(self._data)
except (IndexError, TypeError): # no valid values
vmin_auto = vmax_auto = 0
if min_auto:
vmin = vmin_auto
if max_auto:
vmax = vmax_auto
# Prepare normalizer object
if stretch == 'arcsinh':
stretch = 'asinh'
if stretch == 'log':
if vmid is None:
if vmin < 0:
raise ValueError("When using a log stretch, if vmin < 0, then vmid has to be specified")
else:
vmid = 0.
if vmin < vmid:
raise ValueError("When using a log stretch, vmin should be larger than vmid")
log_a = (vmax - vmid) / (vmin - vmid)
norm_kwargs = {'log_a': log_a}
elif stretch == 'asinh':
if vmid is None:
vmid = vmin - (vmax - vmin) / 30.
asinh_a = (vmid - vmin) / (vmax - vmin)
norm_kwargs = {'asinh_a': abs(asinh_a)}
else:
norm_kwargs = {}
normalizer = simple_norm(self._data, stretch=stretch, power=exponent,
min_cut=vmin, max_cut=vmax, clip=False,
**norm_kwargs)
# Adjust vmin/vmax if auto
if min_auto:
if stretch == 'linear':
vmin = -0.1 * (vmax - vmin) + vmin
log.info("Auto-setting vmin to %10.3e" % vmin)
if max_auto:
if stretch == 'linear':
vmax = 0.1 * (vmax - vmin) + vmax
log.info("Auto-setting vmax to %10.3e" % vmax)
# Update normalizer object
normalizer.vmin = vmin
normalizer.vmax = vmax
if self.image:
self.image.set_visible(True)
self.image.set_norm(normalizer)
self.image.set_cmap(cmap=cmap)
self.image.origin = 'lower'
self.image.set_interpolation(interpolation)
self.image.set_data(convolve_util.convolve(self._data,
smooth=smooth,
kernel=kernel))
else:
extent = -0.5, self._wcs.nx - 0.5, -0.5, self._wcs.ny - 0.5
convolved_data = convolve_util.convolve(self._data, smooth=smooth, kernel=kernel)
self.image = self.ax.imshow(convolved_data, cmap=cmap,
interpolation=interpolation,
origin='lower', norm=normalizer,
aspect=aspect, extent=extent)
xmin, xmax = self.ax.get_xbound()
if xmin == 0.0:
self.ax.set_xlim(0.5, xmax)
ymin, ymax = self.ax.get_ybound()
if ymin == 0.0:
self.ax.set_ylim(0.5, ymax)
if hasattr(self, 'colorbar'):
self.colorbar.update()
[docs] @auto_refresh
def set_nan_color(self, color):
"""
Set the color for NaN pixels.
Parameters
----------
color : str
This can be any valid matplotlib color
"""
from copy import deepcopy
cm = deepcopy(self.image.get_cmap())
cm.set_bad(color)
self.image.set_cmap(cm)
[docs] @auto_refresh
def show_rgb(self, filename=None, interpolation='nearest',
vertical_flip=False, horizontal_flip=False, flip=False):
"""
Show a 3-color image instead of the FITS file data.
Parameters
----------
filename, optional
The 3-color image should have exactly the same dimensions
as the FITS file, and will be shown with exactly the same
projection. If FITSFigure was initialized with an
AVM-tagged RGB image, the filename is not needed here.
vertical_flip : str, optional
Whether to vertically flip the RGB image
horizontal_flip : str, optional
Whether to horizontally flip the RGB image
"""
try:
from PIL import Image
except ImportError:
try:
import Image
except ImportError:
raise ImportError("The Python Imaging Library (PIL) is required to read in RGB images")
if flip:
log.warning("Note that show_rgb should now correctly flip RGB images, so the flip= argument is now deprecated. If you still need to flip an image vertically or horizontally, you can use the vertical_flip= and horizontal_flip arguments instead.")
if filename is None:
if hasattr(self, '_rgb_image'):
image = Image.open(self._rgb_image)
else:
raise Exception("Need to specify the filename of an RGB image")
else:
image = Image.open(filename)
if vertical_flip:
image = image.transpose(Image.FLIP_TOP_BOTTOM)
if horizontal_flip:
image = image.transpose(Image.FLIP_LEFT_RIGHT)
self.image = self.ax.imshow(image,
interpolation=interpolation,
origin='lower')
[docs] @auto_refresh
def show_contour(self, data=None, hdu=0, layer=None, levels=5,
filled=False, cmap=None, colors=None, returnlevels=False,
convention=None, dimensions=[0, 1], slices=[],
smooth=None, kernel='gauss', overlap=False, **kwargs):
"""
Overlay contours on the current plot.
Parameters
----------
data : see below
The FITS file to plot contours for. The following data types can be
passed:
string
astropy.io.fits.PrimaryHDU
astropy.io.fits.ImageHDU
astropy.wcs.WCS
np.ndarray
hdu : int, optional
By default, the image in the primary HDU is read in. If a
different HDU is required, use this argument.
layer : str, optional
The name of the contour layer. This is useful for giving
custom names to layers (instead of contour_set_n) and for
replacing existing layers.
levels : int or list, optional
This can either be the number of contour levels to compute
(if an integer is provided) or the actual list of contours
to show (if a list of floats is provided)
filled : str, optional
Whether to show filled or line contours
cmap : str, optional
The colormap to use for the contours
colors : str or tuple, optional
If a single string is provided, all contour levels will be
shown in this color. If a tuple of strings is provided,
each contour will be colored according to the corresponding
tuple element.
returnlevels : str, optional
Whether to return the list of contours to the caller.
convention : str, optional
This is used in cases where a FITS header can be interpreted
in multiple ways. For example, for files with a -CAR
projection and CRVAL2=0, this can be set to 'wells' or
'calabretta' to choose the appropriate convention.
dimensions : tuple or list, optional
The index of the axes to use if the data has more than three
dimensions.
slices : tuple or list, optional
If a FITS file with more than two dimensions is specified,
then these are the slices to extract. If all extra dimensions
only have size 1, then this is not required.
smooth : int or tuple, optional
Default smoothing scale is 3 pixels across. User can define
whether they want an NxN kernel (integer), or NxM kernel
(tuple). This argument corresponds to the 'gauss' and 'box'
smoothing kernels.
kernel : { 'gauss' , 'box' , numpy.array }, optional
Default kernel used for smoothing is 'gauss'. The user can
specify if they would prefer 'gauss', 'box', or a custom
kernel. All kernels are normalized to ensure flux retention.
overlap str, optional
Whether to include only contours that overlap with the image
area. This significantly speeds up the drawing of contours and
reduces file size when using a file for the contours covering
a much larger area than the image.
kwargs
Additional keyword arguments (such as alpha, linewidths, or
linestyles) will be passed on directly to Matplotlib's
:meth:`~matplotlib.axes.Axes.contour` or
:meth:`~matplotlib.axes.Axes.contourf` methods. For more
information on these additional arguments, see the *Optional
keyword arguments* sections in the documentation for those
methods.
"""
if layer:
self.remove_layer(layer, raise_exception=False)
if cmap:
cmap = plt.cm.get_cmap(cmap)
elif not colors:
cmap = plt.cm.get_cmap('viridis')
if data is not None:
data_contour, header_contour, wcs_contour, wcsaxes_slices = \
self._get_hdu(data, hdu, False, convention=convention,
dimensions=dimensions, slices=slices)
else:
data_contour = self._data
header_contour = self._header
wcs_contour = self._wcs
wcs_contour.nx = header_contour['NAXIS%i' % (dimensions[0] + 1)]
wcs_contour.ny = header_contour['NAXIS%i' % (dimensions[1] + 1)]
image_contour = convolve_util.convolve(data_contour, smooth=smooth, kernel=kernel)
if type(levels) == int:
interval = AsymmetricPercentileInterval(0.25, 99.75, n_samples=10000)
try:
vmin_auto, vmax_auto = interval.get_limits(image_contour)
except IndexError: # no valid values
vmin_auto = vmax_auto = 0
levels = np.linspace(vmin_auto, vmax_auto, levels)
if wcs_contour.wcs.ctype[self.x] == 'PIXEL' or wcs_contour.wcs.ctype[self.y] == 'PIXEL':
frame = 'pixel'
else:
frame = wcs_contour
if filled:
c = self.ax.contourf(image_contour, levels,
transform=self.ax.get_transform(frame),
cmap=cmap,
colors=colors, **kwargs)
else:
c = self.ax.contour(image_contour, levels,
transform=self.ax.get_transform(frame),
cmap=cmap,
colors=colors, **kwargs)
if layer:
contour_set_name = layer
else:
self._contour_counter += 1
contour_set_name = 'contour_set_' + str(self._contour_counter)
self._layers[contour_set_name] = c
if returnlevels:
return levels
[docs] @auto_refresh
def show_vectors(self, pdata, adata, phdu=0, ahdu=0, step=1, scale=1,
rotate=0, cutoff=0, units='degrees', layer=None,
convention=None, dimensions=[0, 1], slices=[], **kwargs):
"""
Overlay vectors on the current plot.
Parameters
----------
pdata : see below
The FITS file specifying the magnitude of vectors. The following data types can be passed:
string
astropy.io.fits.PrimaryHDU
astropy.io.fits.ImageHDU
astropy.wcs.WCS
np.ndarray
adata : see below
The FITS file specifying the angle of vectors. The following data types can be passed:
string
astropy.io.fits.PrimaryHDU
astropy.io.fits.ImageHDU
astropy.wcs.WCS
np.ndarray
phdu : int, optional
By default, the image in the primary HDU is read in. If a
different HDU is required for pdata, use this argument.
ahdu : int, optional
By default, the image in the primary HDU is read in. If a
different HDU is required for adata, use this argument.
step : int, optional
Derive a vector only from every 'step' pixels. You will
normally want this to be >1 to get sensible vector spacing.
scale : int, optional
The length, in pixels, of a vector with magnitude 1 in the image
specified by pdata. If pdata specifies fractional polarization,
make this comparable to step.
rotate : float, optional
An angle to rotate by, in units the same as those of the angle map.
cutoff : float, optional
The value of magnitude below which no
vectors should be plotted. The default value, zero,
excludes negative-length and NaN-masked data.
units : str, optional
Units to assume for the angle map. Valid values are 'degrees'
(the default) or 'radians' (or anything else), which will not
apply a scaling factor of pi/180 to the angle data.
layer : str, optional
The name of the vector layer. This is useful for giving
custom names to layers (instead of vector_set_n) and for
replacing existing layers.
convention : str, optional
This is used in cases where a FITS header can be interpreted
in multiple ways. For example, for files with a -CAR
projection and CRVAL2=0, this can be set to 'wells' or
'calabretta' to choose the appropriate convention.
dimensions : tuple or list, optional
The index of the axes to use if the data has more than three
dimensions.
slices : tuple or list, optional
If a FITS file with more than two dimensions is specified,
then these are the slices to extract. If all extra dimensions
only have size 1, then this is not required.
kwargs
Additional keyword arguments (such as alpha, linewidths, or color)
which are passed to Matplotlib's
:class:`~matplotlib.collections.LineCollection` class, and can be used to
control the appearance of the lines. For more
information on these additional arguments, see the *Optional
keyword arguments* sections in the documentation for those
methods.
"""
# over-ride default color (none) that will otherwise be set by
# show_lines()
if 'color' not in kwargs:
kwargs.setdefault('color', 'black')
if layer:
self.remove_layer(layer, raise_exception=False)
data_p, header_p, wcs_p, slices_p = \
self._get_hdu(pdata, phdu, False, convention=convention,
dimensions=dimensions, slices=slices)
data_a, header_a, wcs_a, slices_a = \
self._get_hdu(adata, ahdu, False, convention=convention,
dimensions=dimensions, slices=slices)
# TODO: use slices correctly
wcs_p.nx = header_p['NAXIS%i' % (dimensions[0] + 1)]
wcs_p.ny = header_p['NAXIS%i' % (dimensions[1] + 1)]
wcs_a.nx = header_a['NAXIS%i' % (dimensions[0] + 1)]
wcs_a.ny = header_a['NAXIS%i' % (dimensions[1] + 1)]
if (wcs_p.nx != wcs_a.nx or wcs_p.ny != wcs_a.ny):
raise Exception("Angle and magnitude images must be same size")
angle = data_a + rotate
if units == 'degrees':
angle = np.radians(angle)
linelist = []
for y in range(0, wcs_p.ny, step):
for x in range(0, wcs_p.nx, step):
if data_p[y, x] > cutoff and np.isfinite(angle[y, x]):
r = data_p[y, x] * 0.5 * scale
a = angle[y, x]
x1 = x + r * np.sin(a)
y1 = y - r * np.cos(a)
x2 = x - r * np.sin(a)
y2 = y + r * np.cos(a)
x_world, y_world = self.pixel2world([x1, x2], [y1, y2], wcs=wcs_p)
line = np.array([x_world, y_world])
linelist.append(line)
if layer:
vector_set_name = layer
else:
self._vector_counter += 1
vector_set_name = 'vector_set_' + str(self._vector_counter)
# Use show_lines to finish the process off
self.show_lines(linelist, layer=vector_set_name, **kwargs)
# This method plots markers. The input should be an Nx2 array with WCS
# coordinates in degree format.
[docs] @auto_refresh
def show_markers(self, xw, yw, layer=False, coords_frame='world', **kwargs):
"""
Overlay markers on the current plot.
Parameters
----------
xw : list or `~numpy.ndarray`
The x positions of the markers (in world coordinates)
yw : list or `~numpy.ndarray`
The y positions of the markers (in world coordinates)
layer : str, optional
The name of the scatter layer. This is useful for giving
custom names to layers (instead of marker_set_n) and for
replacing existing layers.
coords_frame : 'pixel' or 'world'
The reference frame in which the coordinates are defined. This is
used to interpret the values of ``xw`` and ``yw``.
kwargs
Additional keyword arguments (such as marker, facecolor,
edgecolor, alpha, or linewidth) will be passed on directly to
Matplotlib's :meth:`~matplotlib.axes.Axes.scatter` method (in
particular, have a look at the *Optional keyword arguments* in the
documentation for that method).
"""
if 'c' not in kwargs:
kwargs.setdefault('edgecolor', 'red')
kwargs.setdefault('facecolor', 'none')
kwargs.setdefault('s', 30)
if layer:
self.remove_layer(layer, raise_exception=False)
s = self.ax.scatter(xw, yw, transform=self.ax.get_transform(coords_frame), **kwargs)
if layer:
marker_set_name = layer
else:
self._scatter_counter += 1
marker_set_name = 'marker_set_' + str(self._scatter_counter)
self._layers[marker_set_name] = s
# Show circles. Different from markers as this method allows more
# definitions for the circles.
[docs] @auto_refresh
def show_circles(self, xw, yw, radius, layer=False, coords_frame='world', zorder=None, **kwargs):
"""
Overlay circles on the current plot.
Parameters
----------
xw : list or `~numpy.ndarray`
The x positions of the centers of the circles (in world coordinates)
yw : list or `~numpy.ndarray`
The y positions of the centers of the circles (in world coordinates)
radius : int or float or list or `~numpy.ndarray`
The radii of the circles (in world coordinates)
layer : str, optional
The name of the circle layer. This is useful for giving
custom names to layers (instead of circle_set_n) and for
replacing existing layers.
coords_frame : 'pixel' or 'world'
The reference frame in which the coordinates are defined. This is
used to interpret the values of ``xw`` and ``yw``.
kwargs
Additional keyword arguments (such as facecolor, edgecolor, alpha,
or linewidth) are passed to Matplotlib
:class:`~matplotlib.collections.PatchCollection` class, and can be
used to control the appearance of the circles.
"""
xw, yw, radius = uniformize_1d(xw, yw, radius)
if 'facecolor' not in kwargs:
kwargs.setdefault('facecolor', 'none')
if layer:
self.remove_layer(layer, raise_exception=False)
if coords_frame not in ['pixel', 'world']:
raise ValueError("coords_frame should be set to 'pixel' or 'world'")
# While we could plot the shape using the get_transform('world') mode
# from WCSAxes, the issue is that the rotation angle is also measured in
# world coordinates so will not be what the user is expecting. So we allow the user to specify the reference frame for the coordinates and for the rotation.
if coords_frame == 'pixel':
x, y = xw, yw
r = radius
else:
x, y = self.world2pixel(xw, yw)
pix_scale = proj_plane_pixel_scales(self._wcs)
sx, sy = pix_scale[self.x], pix_scale[self.y]
r = radius / np.sqrt(sx * sy)
patches = []
for i in range(len(xw)):
patches.append(Circle((x[i], y[i]), radius=r[i]))
# Due to bugs in matplotlib, we need to pass the patch properties
# directly to the PatchCollection rather than use match_original.
p = PatchCollection(patches, **kwargs)
if zorder is not None:
p.zorder = zorder
c = self.ax.add_collection(p)
if layer:
circle_set_name = layer
else:
self._circle_counter += 1
circle_set_name = 'circle_set_' + str(self._circle_counter)
self._layers[circle_set_name] = c
[docs] @auto_refresh
def show_ellipses(self, xw, yw, width, height, angle=0, layer=False,
zorder=None, coords_frame='world', **kwargs):
"""
Overlay ellipses on the current plot.
Parameters
----------
xw : list or `~numpy.ndarray`
The x positions of the centers of the ellipses (in world coordinates)
yw : list or `~numpy.ndarray`
The y positions of the centers of the ellipses (in world coordinates)
width : int or float or list or `~numpy.ndarray`
The width of the ellipse (in world coordinates)
height : int or float or list or `~numpy.ndarray`
The height of the ellipse (in world coordinates)
angle : int or float or list or `~numpy.ndarray`, optional
rotation in degrees (anti-clockwise). Default
angle is 0.0.
layer : str, optional
The name of the ellipse layer. This is useful for giving
custom names to layers (instead of ellipse_set_n) and for
replacing existing layers.
coords_frame : 'pixel' or 'world'
The reference frame in which the coordinates are defined. This is
used to interpret the values of ``xw``, ``yw``, ``width``, and
``height``.
kwargs
Additional keyword arguments (such as facecolor, edgecolor, alpha,
or linewidth) are passed to Matplotlib
:class:`~matplotlib.collections.PatchCollection` class, and can be
used to control the appearance of the ellipses.
"""
xw, yw, width, height, angle = uniformize_1d(xw, yw, width, height, angle)
if 'facecolor' not in kwargs:
kwargs.setdefault('facecolor', 'none')
if 'edgecolor' not in kwargs:
kwargs.setdefault('edgecolor', 'black')
if layer:
self.remove_layer(layer, raise_exception=False)
if coords_frame not in ['pixel', 'world']:
raise ValueError("coords_frame should be set to 'pixel' or 'world'")
# While we could plot the shape using the get_transform('world') mode
# from WCSAxes, the issue is that the rotation angle is also measured in
# world coordinates so will not be what the user is expecting. So we allow the user to specify the reference frame for the coordinates and for the rotation.
if coords_frame == 'pixel':
x, y = xw, yw
w = width
h = height
a = angle
transform = self.ax.transData
else:
x, y = self.world2pixel(xw, yw)
pix_scale = proj_plane_pixel_scales(self._wcs)
sx, sy = pix_scale[self.x], pix_scale[self.y]
w = width / sx
h = height / sy
a = angle
transform = self.ax.transData
patches = []
for i in range(len(x)):
patches.append(Ellipse((x[i], y[i]), width=w[i], height=h[i], angle=a[i]))
# Due to bugs in matplotlib, we need to pass the patch properties
# directly to the PatchCollection rather than use match_original.
p = PatchCollection(patches, transform=transform, **kwargs)
if zorder is not None:
p.zorder = zorder
c = self.ax.add_collection(p)
if layer:
ellipse_set_name = layer
else:
self._ellipse_counter += 1
ellipse_set_name = 'ellipse_set_' + str(self._ellipse_counter)
self._layers[ellipse_set_name] = c
[docs] @auto_refresh
def show_rectangles(self, xw, yw, width, height, angle=0, layer=False,
zorder=None, coords_frame='world', **kwargs):
"""
Overlay rectangles on the current plot.
Parameters
----------
xw : list or `~numpy.ndarray`
The x positions of the centers of the rectangles (in world coordinates)
yw : list or `~numpy.ndarray`
The y positions of the centers of the rectangles (in world coordinates)
width : int or float or list or `~numpy.ndarray`
The width of the rectangle (in world coordinates)
height : int or float or list or `~numpy.ndarray`
The height of the rectangle (in world coordinates)
angle : int or float or list or `~numpy.ndarray`, optional
rotation in degrees (anti-clockwise). Default
angle is 0.0.
layer : str, optional
The name of the rectangle layer. This is useful for giving
custom names to layers (instead of rectangle_set_n) and for
replacing existing layers.
coords_frame : 'pixel' or 'world'
The reference frame in which the coordinates are defined. This is
used to interpret the values of ``xw``, ``yw``, ``width``, and
``height``.
kwargs
Additional keyword arguments (such as facecolor, edgecolor, alpha,
or linewidth) are passed to Matplotlib
:class:`~matplotlib.collections.PatchCollection` class, and can be
used to control the appearance of the rectangles.
"""
xw, yw, width, height, angle = uniformize_1d(xw, yw, width, height, angle)
if 'facecolor' not in kwargs:
kwargs.setdefault('facecolor', 'none')
if layer:
self.remove_layer(layer, raise_exception=False)
if coords_frame not in ['pixel', 'world']:
raise ValueError("coords_frame should be set to 'pixel' or 'world'")
# While we could plot the shape using the get_transform('world') mode
# from WCSAxes, the issue is that the rotation angle is also measured in
# world coordinates so will not be what the user is expecting. So we
# allow the user to specify the reference frame for the coordinates and
# for the rotation.
if coords_frame == 'pixel':
x, y = xw, yw
w = width
h = height
a = angle
transform = self.ax.transData
else:
x, y = self.world2pixel(xw, yw)
pix_scale = proj_plane_pixel_scales(self._wcs)
sx, sy = pix_scale[self.x], pix_scale[self.y]
w = width / sx
h = height / sy
a = angle
transform = self.ax.transData
x = x - w / 2.
y = y - h / 2.
patches = []
for i in range(len(x)):
patches.append(Rectangle((x[i], y[i]), width=w[i], height=h[i], angle=a[i]))
# Due to bugs in matplotlib, we need to pass the patch properties
# directly to the PatchCollection rather than use match_original.
p = PatchCollection(patches, transform=transform, **kwargs)
if zorder is not None:
p.zorder = zorder
c = self.ax.add_collection(p)
if layer:
rectangle_set_name = layer
else:
self._rectangle_counter += 1
rectangle_set_name = 'rectangle_set_' + str(self._rectangle_counter)
self._layers[rectangle_set_name] = c
[docs] @auto_refresh
def show_lines(self, line_list, layer=False, zorder=None, **kwargs):
"""
Overlay lines on the current plot.
Parameters
----------
line_list : list
A list of one or more 2xN numpy arrays which contain
the [x, y] positions of the vertices in world coordinates.
layer : str, optional
The name of the line(s) layer. This is useful for giving
custom names to layers (instead of line_set_n) and for
replacing existing layers.
kwargs
Additional keyword arguments (such as color, offsets, linestyle,
or linewidth) are passed to Matplotlib
:class:`~matplotlib.collections.LineCollection` class, and can be used to
control the appearance of the lines.
"""
if 'color' not in kwargs:
kwargs.setdefault('color', 'none')
if layer:
self.remove_layer(layer, raise_exception=False)
lines = []
for line in line_list:
xw, yw = line[0, :], line[1, :]
lines.append(np.column_stack((xw, yw)))
lc = LineCollection(lines, transform=self.ax.get_transform('world'), **kwargs)
if zorder is not None:
lc.zorder = zorder
c = self.ax.add_collection(lc)
if layer:
line_set_name = layer
else:
self._linelist_counter += 1
line_set_name = 'line_set_' + str(self._linelist_counter)
self._layers[line_set_name] = c
[docs] @auto_refresh
def show_arrows(self, x, y, dx, dy, width='auto', head_width='auto',
head_length='auto', length_includes_head=True, layer=False,
zorder=None, **kwargs):
"""
Overlay arrows on the current plot.
Parameters
----------
x, y, dx, dy : float or list or `~numpy.ndarray`
Origin and displacement of the arrows in world coordinates.
These can either be scalars to plot a single arrow, or lists or
arrays to plot multiple arrows.
width : float, optional
The width of the arrow body, in pixels (default: 2% of the
arrow length)
head_width : float, optional
The width of the arrow head, in pixels (default: 5% of the
arrow length)
head_length : float, optional
The length of the arrow head, in pixels (default: 5% of the
arrow length)
length_includes_head : bool, optional
Whether the head includes the length
layer : str, optional
The name of the arrow(s) layer. This is useful for giving
custom names to layers (instead of line_set_n) and for
replacing existing layers.
kwargs
Additional keyword arguments (such as facecolor, edgecolor, alpha,
or linewidth) are passed to Matplotlib
:class:`~matplotlib.collections.PatchCollection` class, and can be
used to control the appearance of the arrows.
"""
x, y, dx, dy = uniformize_1d(x, y, dx, dy)
if layer:
self.remove_layer(layer, raise_exception=False)
arrows = []
# Here we don't make use of WCSAxes.get_transform('world') because
# otherwise the arrow heads will be distored. Instead, we work in pixel
# coordinates.
for i in range(len(x)):
xp1, yp1 = self.world2pixel(x[i], y[i])
xp2, yp2 = self.world2pixel(x[i] + dx[i], y[i] + dy[i])
if width == 'auto':
width = 0.02 * np.sqrt((xp2 - xp1) ** 2 + (yp2 - yp1) ** 2)
if head_width == 'auto':
head_width = 0.1 * np.sqrt((xp2 - xp1) ** 2 + (yp2 - yp1) ** 2)
if head_length == 'auto':
head_length = 0.1 * np.sqrt((xp2 - xp1) ** 2 + (yp2 - yp1) ** 2)
arrows.append(FancyArrow(xp1, yp1, xp2 - xp1, yp2 - yp1,
width=width, head_width=head_width,
head_length=head_length,
length_includes_head=length_includes_head)
)
# Due to bugs in matplotlib, we need to pass the patch properties
# directly to the PatchCollection rather than use match_original.
p = PatchCollection(arrows, **kwargs)
if zorder is not None:
p.zorder = zorder
c = self.ax.add_collection(p)
if layer:
line_set_name = layer
else:
self._linelist_counter += 1
line_set_name = 'arrow_set_' + str(self._linelist_counter)
self._layers[line_set_name] = c
[docs] @auto_refresh
def show_polygons(self, polygon_list, layer=False, zorder=None, **kwargs):
"""
Overlay polygons on the current plot.
Parameters
----------
polygon_list : list or tuple
A list of one or more 2xN or Nx2 Numpy arrays which contain
the [x, y] positions of the vertices in world coordinates.
Note that N should be greater than 2.
layer : str, optional
The name of the circle layer. This is useful for giving
custom names to layers (instead of circle_set_n) and for
replacing existing layers.
kwargs
Additional keyword arguments (such as facecolor, edgecolor, alpha,
or linewidth) are passed to Matplotlib
:class:`~matplotlib.collections.PatchCollection` class, and can be
used to control the appearance of the polygons.
"""
if 'facecolor' not in kwargs:
kwargs.setdefault('facecolor', 'none')
if layer:
self.remove_layer(layer, raise_exception=False)
if type(polygon_list) not in [list, tuple]:
raise Exception("polygon_list should be a list or tuple of Numpy arrays")
pix_polygon_list = []
for polygon in polygon_list:
if type(polygon) is not np.ndarray:
raise Exception("Polygon should be given as a Numpy array")
if polygon.shape[0] == 2 and polygon.shape[1] > 2:
xw = polygon[0, :]
yw = polygon[1, :]
elif polygon.shape[0] > 2 and polygon.shape[1] == 2:
xw = polygon[:, 0]
yw = polygon[:, 1]
else:
raise Exception("Polygon should have dimensions 2xN or Nx2 with N>2")
pix_polygon_list.append(np.column_stack((xw, yw)))
patches = []
for i in range(len(pix_polygon_list)):
patches.append(Polygon(pix_polygon_list[i], **kwargs))
# Due to bugs in matplotlib, we need to pass the patch properties
# directly to the PatchCollection rather than use match_original.
p = PatchCollection(patches, transform=self.ax.get_transform('world'), **kwargs)
if zorder is not None:
p.zorder = zorder
c = self.ax.add_collection(p)
if layer:
poly_set_name = layer
else:
self._poly_counter += 1
poly_set_name = 'poly_set_' + str(self._poly_counter)
self._layers[poly_set_name] = c
[docs] @auto_refresh
@fixdocstring
def add_label(self, x, y, text, relative=False, color='black',
family=None, style=None, variant=None, stretch=None,
weight=None, size=None, fontproperties=None,
horizontalalignment='center', verticalalignment='center',
layer=None, **kwargs):
"""
Add a text label.
Parameters
----------
x, y : float
Coordinates of the text label
text : str
The label
relative : str, optional
Whether the coordinates are to be interpreted as world
coordinates (e.g. RA/Dec or longitude/latitude), or
coordinates relative to the axes (where 0.0 is left or bottom
and 1.0 is right or top).
common: color, family, style, variant, stretch, weight, size, fontproperties, horizontalalignment, verticalalignment
"""
if layer:
self.remove_layer(layer, raise_exception=False)
# Can't pass fontproperties=None to text. Only pass it if it is not None.
if fontproperties:
kwargs['fontproperties'] = fontproperties
if not np.isscalar(x):
raise Exception("x should be a single value")
if not np.isscalar(y):
raise Exception("y should be a single value")
if not np.isscalar(text):
raise Exception("text should be a single value")
if relative:
lc = self.ax.text(x, y, text, color=color,
family=family, style=style, variant=variant,
stretch=stretch, weight=weight, size=size,
horizontalalignment=horizontalalignment,
verticalalignment=verticalalignment,
transform=self.ax.transAxes, **kwargs)
else:
lc = self.ax.text(x, y, text, color=color,
family=family, style=style, variant=variant,
stretch=stretch, weight=weight, size=size,
horizontalalignment=horizontalalignment,
verticalalignment=verticalalignment,
transform=self.ax.get_transform('world'),
**kwargs)
if layer:
label_name = layer
else:
self._label_counter += 1
label_name = 'label_' + str(self._label_counter)
self._layers[label_name] = lc
[docs] def set_auto_refresh(self, refresh):
"""
Set whether the display should refresh after each method call.
Parameters
----------
refresh : bool
Whether to refresh the display every time a FITSFigure method is
called. This defaults to `True` if and only if APLpy is being used
from IPython and the Matplotlib backend is interactive.
"""
if refresh is None:
if matplotlib.is_interactive():
try:
get_ipython()
except NameError:
refresh = False
else:
refresh = True
else:
refresh = False
elif not isinstance(refresh, bool):
raise TypeError("refresh argument should be boolean or `None`")
self._figure._auto_refresh = refresh
[docs] def refresh(self, force=True):
"""
Refresh the display.
Parameters
----------
force : str, optional
If set to False, refresh() will only have an effect if
auto refresh is on. If set to True, the display will be
refreshed whatever the auto refresh setting is set to.
The default is True.
"""
if self._figure._auto_refresh or force:
self._figure.canvas.draw()
[docs] def save(self, filename, dpi=None, transparent=False, adjust_bbox=True,
max_dpi=300, format=None):
"""
Save the current figure to a file.
Parameters
----------
filename : str or fileobj
The name of the file to save the plot to. This can be for
example a PS, EPS, PDF, PNG, JPEG, or SVG file. Note that it
is also possible to pass file-like object.
dpi : float, optional
The output resolution, in dots per inch. If the output file
is a vector graphics format (such as PS, EPS, PDF or SVG) only
the image itself will be rasterized. If the output is a PS or
EPS file and no dpi is specified, the dpi is automatically
calculated to match the resolution of the image. If this value is
larger than max_dpi, then dpi is set to max_dpi.
transparent : str, optional
Whether to preserve transparency
adjust_bbox : str, optional
Auto-adjust the bounding box for the output
max_dpi : float, optional
The maximum resolution to output images at. If no maximum is
wanted, enter None or 0.
format : str, optional
By default, APLpy tries to guess the file format based on the
file extension, but the format can also be specified
explicitly. Should be one of 'eps', 'ps', 'pdf', 'svg', 'png'.
"""
if isinstance(filename, str) and format is None:
format = os.path.splitext(filename)[1].lower()[1:]
if dpi is None and format in ['eps', 'ps', 'pdf']:
width = self.ax.get_position().width * self._figure.get_figwidth()
interval = self.ax.get_xlim()
nx = interval[1] - interval[0]
if max_dpi:
dpi = np.minimum(nx / width, max_dpi)
else:
dpi = nx / width
log.info("Auto-setting resolution to %g dpi" % dpi)
if adjust_bbox:
self._figure.savefig(filename, dpi=dpi, transparent=transparent,
bbox_inches='tight', format=format)
else:
self._figure.savefig(filename, dpi=dpi, transparent=transparent,
format=format)
def _initialize_view(self):
self.ax.set_xlim(-0.5, self._wcs.nx - 0.5)
self.ax.set_ylim(-0.5, self._wcs.ny - 0.5)
def _get_invert_default(self):
return self._figure.apl_grayscale_invert_default
def _get_colormap_default(self):
return self._figure.apl_colorscale_cmap_default
[docs] @auto_refresh
def set_theme(self, theme):
"""
Set the axes, ticks, grid, and image colors to a certain style
(experimental).
Parameters
----------
theme : str
The theme to use. At the moment, this can be 'pretty' (for
viewing on-screen) and 'publication' (which makes the ticks
and grid black, and displays the image in inverted grayscale)
"""
if theme == 'pretty':
self.frame.set_color('black')
self.frame.set_linewidth(1.0)
if self.ax.coords[self.x].ticks.get_tick_out():
self.ticks.set_color('black')
else:
self.ticks.set_color('white')
self._figure.apl_grayscale_invert_default = False
self._figure.apl_colorscale_cmap_default = 'viridis'
if self.image:
self.image.set_cmap(cmap=plt.cm.get_cmap('viridis'))
elif theme == 'publication':
self.frame.set_color('black')
self.frame.set_linewidth(1.0)
self.ticks.set_color('black')
self.ticks.set_length(7)
self._figure.apl_grayscale_invert_default = True
self._figure.apl_colorscale_cmap_default = 'gist_heat'
if self.image:
self.image.set_cmap(cmap=plt.cm.get_cmap('gist_yarg'))
[docs] def world2pixel(self, xw, yw, wcs=None):
"""
Convert world to pixel coordinates.
Parameters
----------
xw : float or iterable
x world coordinate
yw : float or iterable
y world coordinate
Returns
-------
xp : float or iterable
x pixel coordinate
yp : float or iterable
y pixel coordinate
"""
if wcs is None:
wcs = self._wcs
return wcs.wcs_world2pix(xw, yw, 0)
[docs] def pixel2world(self, xp, yp, wcs=None):
"""
Convert pixel to world coordinates.
Parameters
----------
xp : float or iterable
x pixel coordinate
yp : float or iterable
y pixel coordinate
Returns
-------
xw : float or iterable
x world coordinate
yw : float or iterable
y world coordinate
"""
if wcs is None:
wcs = self._wcs
return wcs.wcs_pix2world(xp, yp, 0)
[docs] @auto_refresh
def add_grid(self):
"""
Add a coordinate to the current figure.
Once this method has been run, a grid attribute becomes available,
and can be used to control the aspect of the grid::
>>> f = aplpy.FITSFigure(...)
>>> ...
>>> f.add_grid()
>>> f.grid.set_color('white')
>>> f.grid.set_alpha(0.5)
>>> ...
"""
if hasattr(self, 'grid'):
raise Exception("Grid already exists")
try:
self.grid = Grid(self)
self.grid.show()
except Exception:
del self.grid
raise
[docs] @auto_refresh
def remove_grid(self):
"""
Removes the grid from the current figure.
"""
self.grid.hide()
del self.grid
[docs] @auto_refresh
def add_beam(self, *args, **kwargs):
"""
Add a beam to the current figure.
Once this method has been run, a beam attribute becomes available,
and can be used to control the aspect of the beam::
>>> f = aplpy.FITSFigure(...)
>>> ...
>>> f.add_beam()
>>> f.beam.set_color('white')
>>> f.beam.set_hatch('+')
>>> ...
If more than one beam is added, the beam object becomes a list. In
this case, to control the aspect of one of the beams, you will need to
specify the beam index::
>>> ...
>>> f.beam[2].set_hatch('/')
>>> ...
"""
# Initalize the beam and set parameters
b = Beam(self)
b.show(*args, **kwargs)
if hasattr(self, 'beam'):
if type(self.beam) is list:
self.beam.append(b)
else:
self.beam = [self.beam, b]
else:
self.beam = b
[docs] @auto_refresh
def remove_beam(self, beam_index=None):
"""
Removes the beam from the current figure.
If more than one beam is present, the index of the beam should be
specified using beam_index=
"""
if type(self.beam) is list:
if beam_index is None:
raise Exception("More than one beam present - use beam_index= to specify which one to remove")
else:
b = self.beam.pop(beam_index)
b._remove()
del b
# If only one beam is present, remove containing list
if len(self.beam) == 1:
self.beam = self.beam[0]
else:
self.beam._remove()
del self.beam
[docs] @auto_refresh
def add_scalebar(self, length, *args, **kwargs):
"""
Add a scalebar to the current figure.
Once this method has been run, a scalebar attribute becomes
available, and can be used to control the aspect of the scalebar::
>>> f = aplpy.FITSFigure(...)
>>> ...
>>> f.add_scalebar(0.01) # length has to be specified
>>> f.scalebar.set_label('100 AU')
>>> ...
Parameters
----------
length : float, or quantity
The length of the scalebar in degrees, an angular quantity, or angular unit
label : str, optional
Label to place below the scalebar
corner : int, optional
Where to place the scalebar. Acceptable values are:, 'left',
'right', 'top', 'bottom', 'top left', 'top right', 'bottom
left' (default), 'bottom right'
frame : str, optional
Whether to display a frame behind the scalebar (default is False)
kwargs
Additional arguments are passed to the matplotlib Rectangle and
Text classes. See the matplotlib documentation for more details.
In cases where the same argument exists for the two objects, the
argument is passed to both the Text and Rectangle instance.
"""
if hasattr(self, 'scalebar'):
raise Exception("Scalebar already exists")
try:
self.scalebar = Scalebar(self)
self.scalebar.show(length, *args, **kwargs)
except Exception:
del self.scalebar
raise
[docs] @auto_refresh
def remove_scalebar(self):
"""
Removes the scalebar from the current figure.
"""
self.scalebar._remove()
del self.scalebar
[docs] @auto_refresh
def add_colorbar(self, *args, **kwargs):
"""
Add a colorbar to the current figure.
Once this method has been run, a colorbar attribute becomes
available, and can be used to control the aspect of the colorbar::
>>> f = aplpy.FITSFigure(...)
>>> ...
>>> f.add_colorbar()
>>> f.colorbar.set_width(0.3)
>>> f.colorbar.set_location('top')
>>> ...
"""
if hasattr(self, 'colorbar'):
raise Exception("Colorbar already exists")
if self.image is None:
raise Exception("No image is shown, so a colorbar cannot be displayed")
try:
self.colorbar = Colorbar(self)
self.colorbar.show(*args, **kwargs)
except Exception:
del self.colorbar
raise
[docs] @auto_refresh
def remove_colorbar(self):
"""
Removes the colorbar from the current figure.
"""
self.colorbar._remove()
del self.colorbar
savefig = save