# CONTAINS TECHNICAL DATA/COMPUTER SOFTWARE DELIVERED TO THE U.S. GOVERNMENT WITH UNLIMITED RIGHTS
#
# Contract No.: CA 80MSFC17M0022
# Contractor Name: Universities Space Research Association
# Contractor Address: 7178 Columbia Gateway Drive, Columbia, MD 21046
#
# Copyright 2017-2022 by Universities Space Research Association (USRA). All rights reserved.
#
# Developed by: William Cleveland and Adam Goldstein
# Universities Space Research Association
# Science and Technology Institute
# https://sti.usra.edu
#
# Developed by: Daniel Kocevski
# National Aeronautics and Space Administration (NASA)
# Marshall Space Flight Center
# Astrophysics Branch (ST-12)
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
# in compliance with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software distributed under the License
# is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
# implied. See the License for the specific language governing permissions and limitations under the
# License.
#
import re
import copy
from functools import partial
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.ticker as ticker
from matplotlib.colors import colorConverter, PowerNorm
from matplotlib.figure import Figure
from matplotlib.ticker import AutoMinorLocator
from .lib import *
from gdt.core.collection import DataCollection
__all__ = ['DetectorPointing', 'EarthLine', 'EarthPoints', 'EffectiveArea',
'GalacticPlane', 'GdtCmap', 'GdtPlot', 'Heatmap', 'Histo',
'HistoErrorbars', 'HistoFilled', 'LightcurveBackground',
'ModelData', 'ModelSamples', 'PlotElement', 'PlotElementCollection',
'SAA', 'SkyAnnulus', 'SkyHeatmap', 'SkyCircle', 'SkyLine',
'SkyPoints', 'SkyPolygon', 'SpectrumBackground', 'Sun']
[docs]class GdtPlot():
"""Base class for plots in the GDT.
Note:
This class is not intended to be instantiated directly by the user,
rather it is inherited by different plot classes.
Parameters:
figsize ((float, float), optional): The figure size (width, height)
dpi (int, optional): The resolution of the plot. Default is 100
canvas (Canvas Backend object, optional):
If interfacing with a backend (e.g. Tk), pass the relavant
Canvas Backend object and the master widget window. If not set,
uses the default the matplotlib backend.
interactive (bool, optional): If True, then enables interactive plotting
(python environment is available while plot
is displayed). Default is False. This is
overriden if canvas is set.
ax (:class:`matplotlib.axes`, optional):
An existing axes object to plot to. If not set, will create a new
axes object.
**kwargs: Additional options for `Figure.add_subplot
<https://matplotlib.org/3.2.0/api/_as_gen/matplotlib.figure.Figure.html#matplotlib.figure.Figure.add_subplot>`_.
"""
def __init__(self, figsize=None, dpi=100, canvas=None, interactive=False,
ax=None, **kwargs):
self._canvas = None
self._figure = None
if ax is not None:
self._ax = ax
else:
if canvas is not None:
if figsize is None:
figsize = (5, 4)
self._figure = Figure(figsize=figsize, dpi=dpi)
# if tkagg, remember to assign to frame
self._canvas = canvas[0](self._figure, canvas[1])
else:
# just use pyplot, but disable blocking to enable simultaneous
# access to the python command line and dynamic updating
if figsize is None:
figsize = (7.7, 4.7)
self._figure = plt.figure(figsize=figsize, dpi=dpi)
self._canvas = self._figure.canvas
if interactive:
plt.ion()
self._ax = self._figure.add_subplot(111, **kwargs)
self._format_coordinates()
@property
def ax(self):
"""(:class:`matplotlib.axes`): The matplotlib axes object for the plot"""
return self._ax
@property
def canvas(self):
"""(Canvas Backend object): The plotting canvas, if set upon
initialization."""
return self._canvas
@property
def fig(self):
"""(:class:`matplotlib.figure`): The matplotlib figure object"""
return self._figure
@property
def xlim(self):
"""(float, float): The plotting range of the x axis"""
return self._ax.get_xlim()
@xlim.setter
def xlim(self, val):
self._ax.set_xlim(val)
@property
def xscale(self):
"""(str): The scale of the x axis, either 'linear' or 'log'."""
return self._ax.get_xscale()
@xscale.setter
def xscale(self, val):
if (val != 'linear') and (val != 'log'):
raise ValueError("Scale must be either 'linear' or 'log'")
self._ax.set_xscale(val)
@property
def ylim(self):
"""(float, float): The plotting range of the y axis. """
return self._ax.get_ylim()
@ylim.setter
def ylim(self, val):
self._ax.set_ylim(val)
@property
def yscale(self):
"""(str): The scale of the y axis, either 'linear' or 'log'. """
return self._ax.get_yscale()
@yscale.setter
def yscale(self, val):
if (val != 'linear') and (val != 'log'):
raise ValueError("Scale must be either 'linear' or 'log'")
self._ax.set_yscale(val)
def _format_coordinates(self):
# Set the format of the coordinate readout
self._ax.format_coord = lambda x, y: ""
# Set the x minor tick frequency
minorLocator = AutoMinorLocator()
self._ax.xaxis.set_minor_locator(minorLocator)
# Set the y minor tick frequency
minorLocator = AutoMinorLocator()
self._ax.yaxis.set_minor_locator(minorLocator)
[docs]class GdtCmap():
"""A specialized colormap class that implements an alpha gradient.
A callback function can also be defined so that when any of the parameters
are changed, the callback will be executed.
Parameters:
name (str): Any standard Matplotlib colormap name
alpha_min (float, optional): The minimum of the alpha gradient, between
(0, 1) inclusive. Default is 0.0.
alpha_max (float, optional): The maximum of the alpha gradient, between
(0, 1) inclusive. Default is 1.0.
alpha_scale (str, optional): Set to 'linear' for a linear gradient,
or to 'log' for a logarithmic gradient.
Default is 'linear'
callback (function, optional): The callback function to execute when
a parameter is changed.
"""
def __init__(self, name, alpha_min=0.0, alpha_max=1.0, alpha_scale='linear',
callback=None):
self._name = None
self._cmap = None
self._alpha_min = 0.0
self._alpha_max = 1.0
self._alpha_scale = 'linear'
self._callback = None
self.name = name
if alpha_min > alpha_max:
raise ValueError('alpha_min must be <= alpha_max')
self.alpha_min = alpha_min
self.alpha_max = alpha_max
self.alpha_scale = alpha_scale
self.set_callback(callback)
@property
def alpha_max(self):
"""(float): The maximum of the alpha gradient"""
return self._alpha_max
@alpha_max.setter
def alpha_max(self, val):
if val < 0.0 or val > 1.0:
raise ValueError('alpha_max must be >= 0 and <= 1')
if val < self.alpha_min:
raise ValueError('alpha_max must be >= alpha_min')
self._alpha_max = val
self._update()
if self._callback is not None:
self._callback()
@property
def alpha_min(self):
"""(float): The minimum of the alpha gradient"""
if (self.alpha_scale == 'log') and (self._alpha_min == 0):
return 1e-10
else:
return self._alpha_min
@alpha_min.setter
def alpha_min(self, val):
if val < 0.0 or val > 1.0:
raise ValueError('alpha_min must be >= 0 and <= 1')
if val > self.alpha_max:
raise ValueError('alpha_min must be <= alpha_max')
self._alpha_min = val
self._update()
if self._callback is not None:
self._callback()
@property
def alpha_scale(self):
"""(str): The scale of the alpha gradient: linear or log"""
return self._alpha_scale
@alpha_scale.setter
def alpha_scale(self, val):
if val not in ['linear', 'log']:
raise ValueError("alpha_scale can only be 'linear' or 'log'")
self._alpha_scale = val
self._update()
if self._callback is not None:
self._callback()
@property
def cmap(self):
"""(LinearSegmentedColormap): The custom Matplotlib colormap"""
return self._cmap
@property
def name(self):
"""(str): The name of the base colormap"""
return self._name
@name.setter
def name(self, val):
self._cmap = copy.copy(plt.get_cmap(val))
self._name = val
self._update()
if self._callback is not None:
self._callback()
[docs] def set_callback(self, callback):
"""Set the callback function
Args:
callback (function): The callback function
"""
if callback is not None:
try:
callback()
except:
raise RuntimeError('Not a valid callback function')
self._callback = callback
def _update(self):
self._cmap._init()
self._cmap.set_bad(alpha=0.0)
if self.alpha_scale == 'linear':
func = np.linspace
elif self.alpha_scale == 'log':
func = np.geomspace
self._cmap._lut[:-3,-1] = func(self.alpha_min, self.alpha_max,
self._cmap.N)
def __repr__(self):
spaces = ' '*10
s = '<GdtCmap: {};\n'.format(self.name)
s += '{0}alpha_min={1:.2f};\n'.format(spaces, self.alpha_min)
s += '{0}alpha_max={1:.2f};\n'.format(spaces, self.alpha_max)
s += '{0}alpha_scale={1}>'.format(spaces, self.alpha_scale)
return s
[docs]class PlotElement():
"""A base class representing a plot element. A plot element can be a
complex collection of more primitive matplotlib plot elements, but are
treated as a single element.
Note:
This class is not intended to be instantiated directly by the user,
rather it is inherited by child plot element objects.
Parameters:
alpha (float): The alpha opacity value, between 0 and 1.
color (str): The color of the plot element
"""
def __init__(self, color=None, alpha=None):
self._artists = []
self._visible = True
self._color = color
self._alpha = alpha
self._kwargs = None
def __del__(self):
self.remove()
@property
def alpha(self):
"""(float): The alpha opacity value, between 0 and 1."""
return self._alpha
@alpha.setter
def alpha(self, alpha):
[artist.set_alpha(alpha) for artist in self._artists]
self._alpha = alpha
@property
def artists(self):
"""(list): The object references to the individual matplotlib
elements """
return self._artists
@property
def color(self):
"""(str): The color of the plot element"""
return self._color
@color.setter
def color(self, color):
[artist.set_color(color) for artist in self._artists]
self._color = color
@property
def visible(self):
"""(bool): True if the element is shown on the plot, False otherwise"""
return self._visible
@property
def zorder(self):
"""(int): The plot element zorder"""
return self._artists[0].get_zorder()
@zorder.setter
def zorder(self, val):
for artist in self.artists:
artist.set_zorder(val)
[docs] def hide(self):
"""Hide the plot element"""
self._change_visibility(False)
[docs] def remove(self):
"""Remove the plot element"""
for artist in self._artists:
try:
artist.remove()
except:
pass
[docs] def show(self):
"""Show the plot element"""
self._change_visibility(True)
[docs] def toggle(self):
"""Toggle the visibility of the plot element"""
self._change_visibility(not self._visible)
def _change_visibility(self, visible):
for artist in self._artists:
try:
artist.set_visible(visible)
except:
self._set_visible(artist, visible)
self._visible = visible
def _sanitize_artists(self, old_artists):
"""Each artist collection is a collection of artists, and each artist
is a collection of elements. Matplotlib isn't exactly consistent
on how each of these artists are organized for different artist
classes, so we have to do some sanitizing
"""
artists = []
for artist in old_artists:
if artist is None:
continue
elif isinstance(artist, (tuple, list)):
if len(artist) == 0:
continue
artists.extend(self._sanitize_artists(artist))
else:
artists.append(artist)
return artists
def _set_visible(self, artist, visible):
"""In some cases, the artist is a collection of sub-artists and the
set_visible() function has not been exposed to the artists, so
we must iterate.
"""
try:
for subartist in artist.collections:
subartist.set_visible(visible)
except:
pass
[docs]class Histo(PlotElement):
"""Plot a rate histogram of either lightcurves or count spectra.
Parameters:
bins (:class:`~gdt.core.data_primitives.TimeBins` or \
:class:`~gdt.core.data_primitives.EnergyBins`):
The lightcurve or count spectrum histograms
ax (:class:`matplotlib.axes`): The axis on which to plot
color (str, optional): The color of the rate histograms
alpha (float, optional): The alpha of the fill. Default is 1
label (sr, optional): Label for the histogram
**kwargs: Other plotting options
"""
def __init__(self, bins, ax, color='C0', alpha=1.0, label=None, **kwargs):
super().__init__(color=color, alpha=alpha)
self._kwargs = kwargs
artists = self._create(bins, ax)
self._artists = self._sanitize_artists(artists)
if label is not None:
self._artists[0].set_label(label)
@property
def linestyle(self):
"""(str): The linestyle of the histogram. Default is '-'"""
return self._artists[0].get_linestyle()
@linestyle.setter
def linestyle(self, val):
for artist in self._artists:
artist.set_linestyle(val)
@property
def linewidth(self):
"""(int): The line width of the histogram. Default is 1.5"""
return self._artists[0].get_linewidth()
@linewidth.setter
def linewidth(self, val):
for artist in self._artists:
artist.set_linewidth(val)
def _create(self, bins, ax):
return histo(bins, ax, color=self._color, alpha=self._alpha,
**self._kwargs)
def __repr__(self):
spaces = ' '*8
s = '<Histo: color={};\n'.format(self.color)
s += "{0}alpha={1};\n{0}linestyle='{2}';\n".format(spaces, self.alpha,
self.linestyle)
s += '{0}linewidth={1}>'.format(spaces, self.linewidth)
return s
[docs]class HistoErrorbars(PlotElement):
"""Plot errorbars for lightcurves or count spectra.
Parameters:
bins (:class:`~gdt.core.data_primitives.TimeBins` or \
:class:`~gdt.core.data_primitives.EnergyBins`):
The lightcurve or count spectrum histograms
ax (:class:`matplotlib.axes`): The axis on which to plot
color (str, optional): The color of the rate histograms
alpha (float, optional): The alpha of the fill. Default is 1
**kwargs: Other plotting options
"""
def __init__(self, bins, ax, color=None, alpha=None, **kwargs):
super().__init__(color=color, alpha=alpha)
self._kwargs = kwargs
artists = self._create(bins, ax)
self._artists = self._sanitize_artists(artists)
@property
def linewidth(self):
"""(int): The line width of the errorbars. Default is 1.5"""
return self._artists[0].get_linewidth()[0]
@linewidth.setter
def linewidth(self, val):
for artist in self._artists:
artist.set_linewidth(val)
def _create(self, bins, ax):
return histo_errorbars(bins, ax, color=self._color, alpha=self._alpha,
**self._kwargs)
def __repr__(self):
spaces = ' '*17
s = '<HistoErrorbars: color={0};\n{1}'.format(self.color, spaces)
s += 'alpha={0};\n{1}linewidth={2}>'.format(self.alpha, spaces,
self.linewidth)
return s
[docs]class HistoFilled(PlotElement):
"""Plot a filled histogram.
Parameters:
bins (:class:`~gdt.core.data_primitives.TimeBins` or \
:class:`~gdt.core.data_primitives.EnergyBins`):
The lightcurve or count spectrum histograms
ax (:class:`matplotlib.axes`): The axis on which to plot
color (str, optional): The color of the rate histograms
alpha (float, optional): The alpha of the edges. Default is 1
fill_alpha (float, optional): The alpha of the fill. Default is 0.2
**kwargs: Other plotting options
"""
def __init__(self, bins, ax, color=None, alpha=None, fill_alpha=0.2,
**kwargs):
super().__init__(color=color, alpha=alpha)
self._kwargs = kwargs
artists = self._create(bins, ax)
self._artists = self._sanitize_artists(artists)
self.fill_alpha = fill_alpha
@property
def alpha(self):
"""(float): The alpha opacity value of the edge"""
return self._artists[0].get_alpha()
@alpha.setter
def alpha(self, val):
for artist in self._artists[:-1]:
artist.set_alpha(val)
@property
def fill_alpha(self):
"""(float): The alpha opacity value of the fill"""
return self._artists[-1].get_alpha()
@fill_alpha.setter
def fill_alpha(self, val):
self._artists[-1].set_alpha(val)
@property
def linestyle(self):
"""(str): The linestyle of the histogram. Default is '-'"""
return self._artists[0].get_linestyle()
@linestyle.setter
def linestyle(self, val):
for artist in self._artists:
artist.set_linestyle(val)
@property
def linewidth(self):
"""(int): The line width of the histogram. Default is 1.5"""
return self._artists[0].get_linewidth()
@linewidth.setter
def linewidth(self, val):
for artist in self._artists:
artist.set_linewidth(val)
def _create(self, bins, ax):
return histo_filled(bins, ax, color=self._color,
fill_alpha=self._alpha,
**self._kwargs)
def __repr__(self):
spaces = ' '*14
s = '<HistoFilled: color={0};\n{1}'.format(self.color, spaces)
s += 'alpha={0};\n{1}fill_alpha={2};\n{1}'.format(self.alpha, spaces,
self.fill_alpha)
s += "linestyle='{0}';\n{1}linewidth={2}>".format(self.linestyle,
spaces,self.linewidth)
return s
[docs]class LightcurveBackground(PlotElement):
"""Plot a lightcurve background model with an error band.
Parameters:
backrates (:class:`~gdt.background.primitives.BackgroundRates`):
The background rates object integrated over energy. If there is
more than one remaining energy channel, the background will be
integrated over the remaining energy channels.
ax (:class:`matplotlib.axes`): The axis on which to plot
color (str, optional): The color of the background.
alpha (float, optional): The alpha of the background central line.
band_alpha (float, optional): The alpha of the background uncertainty.
**kwargs: Other plotting options
"""
def __init__(self, backrates, ax, color=None, alpha=None, band_alpha=None,
**kwargs):
super().__init__(color=color, alpha=alpha)
self._kwargs = kwargs
artists = self._create(backrates, ax)
self._artists = self._sanitize_artists(artists)
self.band_alpha = band_alpha
@property
def alpha(self):
"""(float): The alpha opacity value"""
return self._artists[0].get_alpha()
@alpha.setter
def alpha(self, alpha):
self._artists[0].set_alpha(alpha)
@property
def band_alpha(self):
"""(float): The opacity of the uncertainty band"""
return self._artists[1].get_alpha()
@band_alpha.setter
def band_alpha(self, alpha):
return self._artists[1].set_alpha(alpha)
@property
def linestyle(self):
"""(str): The linestyle of the background Default is '-'"""
return self._artists[0].get_linestyle()
@linestyle.setter
def linestyle(self, val):
self._artists[0].set_linestyle(val)
@property
def linewidth(self):
"""(int): The line width of the background. Default is 1.5"""
return self._artists[0].get_linewidth()
@linewidth.setter
def linewidth(self, val):
self._artists[0].set_linewidth(val)
def _create(self, backrates, ax):
return lightcurve_background(backrates, ax, cent_color=self.color,
err_color=self.color,
cent_alpha=self._alpha,
err_alpha=self._alpha,
**self._kwargs)
def __repr__(self):
spaces = ' '*23
s = '<LightcurveBackground: color={0};\n{1}'.format(self.color, spaces)
s += 'alpha={0};\n{1}band_alpha={2};\n{1}'.format(self.alpha, spaces,
self.band_alpha)
s += "linestyle='{0}';\n{1}linewidth={2}>".format(self.linestyle,
spaces,self.linewidth)
return s
[docs]class SpectrumBackground(PlotElement):
"""Plot a count spectrum background model with an error band.
Parameters:
backspec (:class:`~gdt.background.primitives.BackgroundSpectrum`):
The background sepctrum object integrated over time
ax (:class:`matplotlib.axes`): The axis on which to plot
cent_alpha (float, optional): The alpha of the background centroid line.
Default is 1
cent_color (str, optional): The color of the background centroid line
err_alpha (float, optional): The alpha of the background uncertainty.
Default is 1
err_color (str, optional): The color of the background uncertainty
color (str, optional): The color of the background. If set, overrides
``cent_color`` and ``err_color``.
alpha (float, optional): The alpha of the background. If set, overrides
``cent_alpha`` and ``err_alpha``
**kwargs: Other plotting options
"""
def __init__(self, backspec, ax, color=None, alpha=None, band_alpha=None,
**kwargs):
super().__init__(color=color, alpha=alpha)
self._kwargs = kwargs
artists = self._create(backspec, ax)
self._artists = self._sanitize_artists(artists)
self.band_alpha = band_alpha
@property
def alpha(self):
"""(float): The alpha opacity value"""
return self._artists[0].get_alpha()
@alpha.setter
def alpha(self, alpha):
self._artists[0].set_alpha(alpha)
@property
def band_alpha(self):
"""(float): The opacity of the uncertainty band"""
return self._artists[1].get_alpha()
@band_alpha.setter
def band_alpha(self, alpha):
return self._artists[1].set_alpha(alpha)
@property
def linestyle(self):
"""(str): The linestyle of the background Default is '-'"""
return self._artists[0].get_linestyle()
@linestyle.setter
def linestyle(self, val):
self._artists[0].set_linestyle(val)
@property
def linewidth(self):
"""(int): The line width of the background. Default is 1.5"""
return self._artists[0].get_linewidth()
@linewidth.setter
def linewidth(self, val):
self._artists[0].set_linewidth(val)
def _create(self, backspec, ax):
return spectrum_background(backspec, ax, cent_color=self.color,
err_color=self.color, cent_alpha=self._alpha,
err_alpha=self._alpha, **self._kwargs)
def __repr__(self):
spaces = ' '*21
s = '<SpectrumBackground: color={0};\n{1}'.format(self.color, spaces)
s += 'alpha={0};\n{1}band_alpha={2};\n{1}'.format(self.alpha, spaces,
self.band_alpha)
s += "linestyle='{0}';\n{1}linewidth={2}>".format(self.linestyle,
spaces,self.linewidth)
return s
[docs]class Heatmap(PlotElement):
"""Plot a general heatmap (color gradient). By default, the heatmap opacity
will scale from 0 (fully transparent) to 1 (fully opaque) corresponding to
the colormap value, creating an opacity gradient. This behavior can be
adjusted by setting ``alpha_min`` and ``alpha_max``.
Parameters:
x (np.array): The x-coordinate array of the heatmap grid
y (np.array): The y-coordinate array of the heatmap grid
heatmap (np.array): The heatmap array, of shape (``x.size``, ``y.size``)
ax (:class:`matplotlib.axes`): The axis on which to plot
colorbar (bool, optional): If True, add the colorbar scale.
Default is True.
color (:class:`GdtCmap`): The colormap of the heatmap.
Default is 'viridis'
norm (:class:`matplotlib.colors.Normalize` or similar, optional):
The normalization used to scale the colormapping to the heatmap
values. This can be initialized by the defined matplotlib
normalizations or a custom normalization.
num_contours (int, optional): The number of contour lines to draw.
Default is 100.
**kwargs: Other plotting options
"""
def __init__(self, x, y, heatmap, ax, colorbar=True, color=GdtCmap('viridis'),
alpha=None, norm=None, num_contours=100, **kwargs):
# store data for replotting if needed
self._x = x
self._y = y
self._heatmap = heatmap
self._ax = ax
self._colorbar = colorbar
self._num_contours = num_contours
# set the normalization, and ensure that it is scaled to the data range
if norm is None:
norm = PowerNorm(gamma=0.3)
self._norm = norm
self._norm.vmin = self._heatmap.min()
self._norm.vmax = self._heatmap.max()
super().__init__(color=color, alpha=alpha)
self._kwargs = kwargs
artists = self._create()
self._artists = self._sanitize_artists(artists)
# set the colormap
self.color = color
self.color.set_callback(self._artists[0].changed)
@property
def color(self):
"""(GdtCmap): The colormap"""
return self._color
@color.setter
def color(self, color):
if not isinstance(color, GdtCmap):
raise TypeError('color must be of type GdtCmap')
self._color = color
self._artists[0].set_cmap(color.cmap)
@property
def colorbar(self):
"""(matplotlib.colorbar.Colorbar): The colorbar object"""
if self._colorbar:
return self._artists[-1]
@property
def norm(self):
"""(matplotlib.colors.Normalize or similar): The colormap normalization"""
return self._norm
@norm.setter
def norm(self, norm):
# mark FIXME: This creates a new colorbar. Need to force it to update
# or go through _make_colorbar()
norm.vmin = self._heatmap.min()
norm.vmax = self._heatmap.max()
self._artists[0].set_norm(norm)
self._norm = norm
@property
def num_contours(self):
"""(int): Number of plot contours"""
# mark TODO: Add a setter, which will have to do a replot
return self._num_contours
def _create(self):
refs = response_matrix(self._x, self._y, self._heatmap, self._ax,
cmap=self._color.cmap, norm=self._norm,
num_contours=self._num_contours, **self._kwargs)
refs = [refs]
if self._colorbar:
refs.append(self._make_colorbar(self._ax, refs[0]))
return refs
def _make_colorbar(self, ax, artist):
# scientific format for the colorbar
def sci_fmt(x, pos):
if x >= 1e-3:
return r'{:.3f}'.format(x)
else:
a, b = '{:.0e}'.format(x).split('e')
b = int(b)
return r'${} \times 10^{{{}}}$'.format(a, b)
# vertical colorbar
cb = plt.colorbar(artist, ax=ax, aspect=10.0, pad=0.01, panchor=False,
cmap=self._color.cmap, norm=self._norm,
orientation='vertical',
format=ticker.FuncFormatter(sci_fmt))
cb.ax.tick_params(labelsize=10)
cb.set_label(r'Effective Area (cm$^2$)', fontsize=12)
cb._draw_all()
return cb
def __repr__(self):
spaces = ' ' * 10
s = "<Heatmap: color='{0}';\n{1}".format(self.color.name, spaces)
s += 'norm={0};\n{1}'.format(self.norm.__class__.__name__, spaces)
s += 'num_contours={0};\n{1}'.format(self.num_contours, spaces)
s += 'colorbar={0}>'.format(self._colorbar)
return s
[docs]class EffectiveArea(PlotElement):
"""Plot a histogram of the effective area of a detector response.
Parameters:
bins (:class:`~gdt.core.data_primitives.Bins`): The effective area
histograms
ax (:class:`matplotlib.axes`): The axis on which to plot
color (str, optional): The color of the rate histograms
alpha (float, optional): The alpha of the histogram. Default is 1
**kwargs: Other plotting options
"""
def __init__(self, bins, ax, color='C0', alpha=1.0, orientation='vertical',
**kwargs):
super().__init__(color=color, alpha=alpha)
self._kwargs = kwargs
self._orientation = orientation
artists = self._create(bins, ax)
self._artists = self._sanitize_artists(artists)
@property
def linestyle(self):
"""(str): The linestyle of the histogram. Default is '-'"""
return self._artists[0].get_linestyle()
@linestyle.setter
def linestyle(self, val):
for artist in self._artists:
artist.set_linestyle(val)
@property
def linewidth(self):
"""(int): The line width of the histogram. Default is 1.5"""
return self._artists[0].get_linewidth()
@linewidth.setter
def linewidth(self, val):
for artist in self._artists:
artist.set_linewidth(val)
def _create(self, bins, ax):
return effective_area(bins, ax, color=self._color, alpha=self._alpha,
orientation=self._orientation, **self._kwargs)
def __repr__(self):
spaces = ' ' * 16
s = '<EffectiveArea: color={};\n'.format(self.color)
s += "{0}alpha={1};\n{0}linestyle='{2}';\n".format(spaces, self.alpha,
self.linestyle)
s += '{0}linewidth={1}>'.format(spaces, self.linewidth)
return s
[docs]class SAA(PlotElement):
"""Plot the SAA polygon on the Earth.
Parameters:
saa_def (:class:`~gdt.core.geomagnetic.SouthAtlanticAnomaly`):
Object containing the SAA boundary definition
proj (Cartopy Projection): The Cartopy projection
ax (:class:`matplotlib.axes`): The axis on which to plot
color (str, optional): The color of the SAA polygon
alpha (float, optional): The alpha of the interior of the polygon
**kwargs: Other plotting options
"""
def __init__(self, saa_def, proj, ax, color='darkred', alpha=0.4, **kwargs):
super().__init__(color=color, alpha=alpha)
self._kwargs = kwargs
artists = self._create(saa_def, proj, ax)
self._artists = self._sanitize_artists(artists)
self._path = artists[0].get_path()
@property
def fill(self):
"""(bool): True if the polygon is filled, False otherwise"""
return [artist.get_fill() for artist in self.artists \
if artist.__class__.__name__ == 'Polygon'][0]
@fill.setter
def fill(self, val):
for artist in self.artists:
if artist.__class__.__name__ == 'Polygon':
artist.set_fill(val)
@property
def hatch(self):
"""(str): The hatching string"""
return [artist.get_hatch() for artist in self.artists \
if artist.__class__.__name__ == 'Polygon'][0]
@hatch.setter
def hatch(self, val):
for artist in self.artists:
if artist.__class__.__name__ == 'Polygon':
artist.set_hatch(val)
@property
def linestyle(self):
"""(str): The linestyle of the edge"""
return [artist.get_linestyle() for artist in self.artists \
if artist.__class__.__name__ == 'Polygon'][0]
@linestyle.setter
def linestyle(self, val):
for artist in self.artists:
if artist.__class__.__name__ == 'Polygon':
artist.set_linestyle(val)
@property
def linewidth(self):
"""(int): The line width of the edge"""
return [artist.get_linewidth() for artist in self.artists \
if artist.__class__.__name__ == 'Polygon'][0]
@linewidth.setter
def linewidth(self, val):
for artist in self.artists:
if artist.__class__.__name__ == 'Polygon':
artist.set_linewidth(val)
[docs] def in_saa(self, lon, lat):
"""Check if a point or points is inside the SAA
Args:
lon (np.array): longitudes
lat (np.array): latitudes
Returns:
np.array: Boolean array for each point where True indicates the \
point is in the SAA.
"""
pts = np.array((lon.ravel(), lat.ravel())).T
mask = self._path.contains_points(pts)
return mask
def _create(self, saa_def, proj, ax):
artists = saa_polygon(saa_def.latitude, saa_def.longitude, proj,
color=self._color, alpha=self._alpha,
**self._kwargs)
ax.add_patch(artists)
return [artists]
def __repr__(self):
spaces = ' '*6
s = "<SAA: color='{}';\n".format(self.color)
s += "{0}alpha={1};\n".format(spaces, self.alpha)
s += "{0}linestyle='{1}';\n".format(spaces, self.linestyle)
s += '{0}linewidth={1}>'.format(spaces, self.linewidth)
return s
[docs]class EarthLine(PlotElement):
"""Plot a line on the Earth.
Parameters:
lat (np.array): The latitude values of the line
lon (np.array): The longitude values of the line
proj (GeoAxesSubplot): The Cartopy projection
color (str, optional): The color of the line
alpha (float, optional): The alpha opacity of the line
**kwargs: Other plotting options
"""
def __init__(self, lat, lon, proj, color='black', alpha=0.4, **kwargs):
super().__init__(color=color, alpha=alpha)
self._kwargs = kwargs
artists = self._create(lat, lon, proj)
self._artists = self._sanitize_artists(artists)
@property
def linestyle(self):
"""(str): The linestyle of the histogram. Default is '-'"""
return self._artists[0].get_linestyle()
@linestyle.setter
def linestyle(self, val):
for artist in self._artists:
artist.set_linestyle(val)
@property
def linewidth(self):
"""(int): The line width of the histogram. Default is 1.5"""
return self._artists[0].get_linewidth()
@linewidth.setter
def linewidth(self, val):
for artist in self._artists:
artist.set_linewidth(val)
def _create(self, lat, lon, proj):
artists = earth_line(lat, lon, proj, color=self._color,
alpha=self._alpha, **self._kwargs)
return artists
def __repr__(self):
spaces = ' ' * 12
s = '<EarthLine: color={};\n'.format(self.color)
s += "{0}alpha={1};\n{0}linestyle='{2}';\n".format(spaces, self.alpha,
self.linestyle)
s += '{0}linewidth={1}>'.format(spaces, self.linewidth)
return s
[docs]class EarthPoints(PlotElement):
"""Plot a point or set of points on the Earth.
Parameters:
lat (np.array): The latitude values
lon (np.array): The longitude values
proj (GeoAxesSubplot): The Cartopy projection
color (str, optional): The color of the line
alpha (float, optional): The alpha opacity of the line
sizes (list, optional): The sizes of the plot points. Default is 10.
**kwargs: Other plotting options
"""
def __init__(self, lat, lon, proj, color='black', alpha=1.0, sizes=10,
**kwargs):
super().__init__(color=color, alpha=alpha)
self._kwargs = kwargs
artists = self._create(lat, lon, proj)
self._artists = self._sanitize_artists(artists)
self._lat = lat
self._lon = lon
self.sizes = sizes
@property
def coordinates(self):
"""(list of str): The formatted coordinate list of the points"""
coords = self._to_geocoords()
return coords
@property
def num_points(self):
"""(int): Number of plotted points"""
return len(self._artists[0].get_offsets())
@property
def sizes(self):
"""(list): The size of each plotted point"""
try:
return self._artists[0].get_sizes()
except:
return [1]
@sizes.setter
def sizes(self, val):
if isinstance(val, (int, float)):
val = [val] * self.num_points
try:
self._artists[0].set_sizes(val)
except:
pass
def _create(self, lat, lon, proj):
artists = earth_points(lat, lon, proj, color=self._color,
alpha=self._alpha, **self._kwargs)
return artists
def _to_geocoords(self):
lat_formatter = lambda x: "%.2fN" % (x) if x > 0 else "%.2fS" % (-x)
lon_formatter = lambda x: "%.2fE" % (x) if x > 0 else "%.2fW" % (-x)
lat = lat_formatter(self._lat)
lon = lon_formatter(self._lon)
return (lat, lon)
def __repr__(self):
spaces = ' ' * 14
s = '<EarthPoints: {0} points;\n{1}'.format(self.num_points, spaces)
s += "color='{0}';\n{1}alpha={2}>".format(self.color, spaces, self.alpha)
return s
[docs]class SkyPoints(PlotElement):
"""Plot a set of points on the sky in equatorial, galactic, or spacecraft
coordinates.
Parameters:
x (float or np.array):
The azimuthal coordinate, in degrees
y (float or np.array):
The polar coordinate, in degrees
ax (:class:`matplotlib.axes`): The axis on which to plot
flipped (bool, optional): If True, the azimuthal axis is flipped,
following equatorial convention
frame (str, optional): Either 'equatorial', 'galactic', 'spacecraft'.
Default is 'equatorial'
color (str, optional): The color of the points
alpha (float, optional): The alpha opacity of the points
**kwargs: Other plotting options
"""
def __init__(self, x, y, ax, flipped=True, frame='equatorial', color='C0',
alpha=1.0, **kwargs):
super().__init__(color=color, alpha=alpha)
self._kwargs = kwargs
artists = self._create(x, y, ax, flipped, frame)
self._artists = self._sanitize_artists(artists)
@property
def num_points(self):
"""(int): The number of points plotted"""
return len(self._artists[0].get_offsets())
@property
def sizes(self):
"""(list): The size of each plot point"""
return self._artists[0].get_sizes()
@sizes.setter
def sizes(self, val):
if isinstance(val, (int, float)):
val = [val] * self.num_points
self._artists[0].set_sizes(val)
def _create(self, x, y, ax, flipped, frame):
ref = sky_point(x, y, ax, flipped=flipped, frame=frame,
color=self._color, alpha=self._alpha, **self._kwargs)
return [ref]
def __repr__(self):
spaces = ' '*12
s = '<SkyPoints: {0} points;\n{1}'.format(self.num_points, spaces)
s += "color='{0}';\n{1}alpha={2}>".format(self.color, spaces, self.alpha)
return s
[docs]class SkyLine(PlotElement):
"""Plot a line on the sky in either equatorial, galactic, or spacecraft
coordinates.
Parameters:
x (np.array): The azimuthal coordinate, in degrees
y (np.array): The polar coordinate, in degrees
ax (:class:`matplotlib.axes`): The axis on which to plot
flipped (bool, optional): If True, the azimuthal axis is flipped,
following equatorial convention
frame (str, optional): Either 'equatorial', 'galactic', 'spacecraft'.
Default is 'equatorial'
color (str, optional): The color of the line
alpha (float, optional): The alpha opacity of the line
**kwargs: Other plotting options
"""
def __init__(self, x, y, ax, flipped=True, frame='equatorial', color='C0',
alpha=1.0, **kwargs):
super().__init__(color=color, alpha=alpha)
self._kwargs = kwargs
artists = self._create(x, y, ax, flipped, frame)
self._artists = self._sanitize_artists(artists)
@property
def linestyle(self):
"""(str): The linestyle of the histogram. Default is '-'"""
return self._artists[0].get_linestyle()
@linestyle.setter
def linestyle(self, val):
for artist in self._artists:
artist.set_linestyle(val)
@property
def linewidth(self):
"""(int): The line width of the histogram. Default is 1.5"""
return self._artists[0].get_linewidth()
@linewidth.setter
def linewidth(self, val):
for artist in self._artists:
artist.set_linewidth(val)
def _create(self, x, y, ax, flipped, frame):
refs = sky_line(x, y, ax, flipped=flipped, frame=frame,
color=self._color, alpha=self._alpha, **self._kwargs)
return refs
def __repr__(self):
spaces = ' '*10
s = '<SkyLine: color={};\n'.format(self.color)
s += "{0}alpha={1};\n{0}linestyle='{2}';\n".format(spaces, self.alpha,
self.linestyle)
s += '{0}linewidth={1}>'.format(spaces, self.linewidth)
return s
[docs]class SkyCircle(PlotElement):
"""Plot a circle on the sky in equatorial, galactic, or spacecraft
coordinates.
Parameters:
x (float): The azimuthal coordinate of the center, in degrees
y (float): The polar coordinate of the center, in degrees
radius (float): The radius of the circle, in degrees
ax (:class:`matplotlib.axes`): The axis on which to plot
flipped (bool, optional): If True, the azimuthal axis is flipped,
following equatorial convention
frame (str, optional): Either 'equatorial', 'galactic', 'spacecraft'.
Default is 'equatorial'
face_color (str, optional): The color of the circle fill
face_alpha (float, optional): The alpha opacity of the circle fill
edge_color (str, optional): The color of the circle edge
edge_alpha (float, optional): The alpha opacity of the circle edge
color (str, optional): The color of the circle. If set, overrides
``face_color`` and ``edge_color``.
alpha (float, optional): The alpha of the circle. If set, overrides
``face_alpha`` and ``edge_alpha``.
**kwargs: Other plotting options
"""
def __init__(self, x, y, radius, ax, flipped=True, frame='equatorial',
color='C0', alpha=1.0, face_color=None, face_alpha=None,
edge_color=None, edge_alpha=None, **kwargs):
super().__init__(color=color, alpha=alpha)
self._kwargs = kwargs
# color and alpha act as setting the global color values for the object
if color is not None:
face_color = color
edge_color = color
if alpha is not None:
face_alpha = alpha
edge_alpha = alpha
self._face_alpha = face_alpha
self._edge_alpha = edge_alpha
self._face_color = face_color
self._edge_color = edge_color
artists = self._create(x, y, radius, ax, flipped, frame)
self._artists = self._sanitize_artists(artists)
@property
def alpha(self):
"""(float): The alpha opacity value"""
return self._alpha
@alpha.setter
def alpha(self, alpha):
self.face_alpha = alpha
self.edge_alpha = alpha
self._alpha = alpha
@property
def color(self):
"""(str): The color of the circle"""
return self._color
@color.setter
def color(self, color):
self.face_color = color
self.edge_color = color
self._color = color
@property
def edge_alpha(self):
"""(float): The alpha opacity of the circle edge"""
return self._edge_alpha
@edge_alpha.setter
def edge_alpha(self, alpha):
edge = colorConverter.to_rgba(self._edge_color, alpha=alpha)
[artist.set_edgecolor(edge) for artist in self._artists \
if hasattr(artist, 'set_edgecolor')]
self._edge_alpha = alpha
@property
def edge_color(self):
"""(str): The color of the circle edge"""
return self._edge_color
@edge_color.setter
def edge_color(self, color):
edge = colorConverter.to_rgba(color, alpha=self._edge_alpha)
[artist.set_edgecolor(edge) for artist in self._artists \
if hasattr(artist, 'set_edgecolor')]
self._edge_color = color
@property
def face_alpha(self):
"""(float): The alpha opacity of the circle fill"""
return self._face_alpha
@face_alpha.setter
def face_alpha(self, alpha):
face = colorConverter.to_rgba(self._face_color, alpha=alpha)
[artist.set_facecolor(face) for artist in self._artists \
if hasattr(artist, 'set_facecolor')]
self._face_alpha = alpha
@property
def face_color(self):
"""(str): The color of the circle fill"""
return self._face_color
@face_color.setter
def face_color(self, color):
face = colorConverter.to_rgba(color, alpha=self._face_alpha)
[artist.set_facecolor(face) for artist in self._artists \
if hasattr(artist, 'set_facecolor')]
self._face_color = color
@property
def fill(self):
"""(bool): True if the circle is filled, False otherwise"""
return self._artists[0].get_fill()
@fill.setter
def fill(self, val):
for artist in self._artists:
try:
artist.set_fill(val)
except:
pass
@property
def hatch(self):
"""(str): The hatching string"""
return self._artists[0].get_hatch()
@hatch.setter
def hatch(self, val):
for artist in self._artists:
try:
artist.set_hatch(val)
except:
pass
@property
def linestyle(self):
"""(str): The linestyle of the edge"""
return self._artists[0].get_linestyle()
@linestyle.setter
def linestyle(self, val):
for artist in self._artists:
try:
artist.set_linestyle(val)
except:
pass
@property
def linewidth(self):
"""(int): The line width of the edge"""
return self._artists[0].get_linewidth()
@linewidth.setter
def linewidth(self, val):
for artist in self._artists:
try:
artist.set_linewidth(val)
except:
pass
def _create(self, x, y, radius, ax, flipped, frame):
refs = sky_circle(x, y, radius, ax, flipped=flipped, frame=frame,
face_color=self._face_color,
face_alpha=self._face_alpha,
edge_color=self._edge_color,
edge_alpha=self._edge_alpha,
**self._kwargs)
return refs
def __repr__(self):
spaces = ' '*12
s = '<SkyCircle: face_color={};\n'.format(self.face_color)
s += "{0}face_alpha={1};\n".format(spaces, self.face_alpha)
s += "{0}edge_color={1};\n".format(spaces, self.edge_color)
s += "{0}edge_alpha={1};\n".format(spaces, self.edge_alpha)
s += "{0}linestyle='{1}';\n".format(spaces, self.linestyle)
s += '{0}linewidth={1}>'.format(spaces, self.linewidth)
return s
[docs]class SkyPolygon(PlotElement):
"""Plot a polygon on the sky in equatorial, galactic, or spacecraft
coordinates.
Parameters:
x (float): The azimuthal coordinate, in degrees
y (float): The polar coordinate, in degrees
ax (:class:`matplotlib.axes`): The axis on which to plot
flipped (bool, optional): If True, the azimuthal axis is flipped,
following equatorial convention
frame (str, optional): Either 'equatorial', 'galactic', 'spacecraft'.
Default is 'equatorial'
face_color (str, optional): The color of the polygon fill
face_alpha (float, optional): The alpha opacity of the polygon fill
edge_color (str, optional): The color of the polygon edge
edge_alpha (float, optional): The alpha opacity of the polygon edge
color (str, optional): The color of the polygon. If set, overrides
``face_color`` and ``edge_color``.
alpha (float, optional): The alpha of the polygon. If set, overrides
``face_alpha`` and ``edge_alpha``.
**kwargs: Other plotting options
"""
def __init__(self, x, y, ax, flipped=True, frame='equatorial', color='C0',
alpha=1.0, face_color=None, face_alpha=None, edge_color=None,
edge_alpha=None, **kwargs):
super().__init__(color=color, alpha=alpha)
self._kwargs = kwargs
# color and alpha act as setting the global color values for the object
if color is not None:
face_color = color
edge_color = color
if alpha is not None:
face_alpha = alpha
edge_alpha = alpha
self._face_alpha = face_alpha
self._edge_alpha = edge_alpha
self._face_color = face_color
self._edge_color = edge_color
artists = self._create(x, y, ax, flipped, frame)
self._artists = self._sanitize_artists(artists)
@property
def alpha(self):
"""(float): The alpha opacity value"""
return self._alpha
@alpha.setter
def alpha(self, alpha):
self.face_alpha = alpha
self.edge_alpha = alpha
self._alpha = alpha
@property
def color(self):
"""(str): The color of the polygon"""
return self._color
@color.setter
def color(self, color):
self.face_color = color
self.edge_color = color
self._color = color
@property
def edge_alpha(self):
"""(float): The alpha opacity of the polygon edge"""
return self._edge_alpha
@edge_alpha.setter
def edge_alpha(self, alpha):
for artist in self.artists:
if artist.__class__.__name__ == 'Line2D':
artist.set_alpha(alpha)
self._edge_alpha = alpha
@property
def edge_color(self):
"""(str): The color of the polygon edge"""
return self._edge_color
@edge_color.setter
def edge_color(self, color):
for artist in self.artists:
if artist.__class__.__name__ == 'Line2D':
artist.set_color(color)
self._edge_color = color
@property
def face_alpha(self):
"""(float): The alpha opacity of the polygon fill"""
return self._face_alpha
@face_alpha.setter
def face_alpha(self, alpha):
for artist in self.artists:
if artist.__class__.__name__ == 'Polygon':
artist.set_alpha(alpha)
self._face_alpha = alpha
@property
def face_color(self):
"""(str): The color of the polygon fill"""
return self._face_color
@face_color.setter
def face_color(self, color):
for artist in self.artists:
if artist.__class__.__name__ == 'Polygon':
artist.set_color(color)
self._face_color = color
@property
def fill(self):
"""(bool): True if the polygon is filled, False otherwise"""
return [artist.get_fill() for artist in self.artists \
if artist.__class__.__name__ == 'Polygon'][0]
@fill.setter
def fill(self, val):
for artist in self.artists:
if artist.__class__.__name__ == 'Polygon':
artist.set_fill(val)
@property
def hatch(self):
"""(str): The hatching string"""
return [artist.get_hatch() for artist in self.artists \
if artist.__class__.__name__ == 'Polygon'][0]
@hatch.setter
def hatch(self, val):
for artist in self.artists:
if artist.__class__.__name__ == 'Polygon':
artist.set_hatch(val)
@property
def linestyle(self):
"""(str): The linestyle of the edge"""
return [artist.get_linestyle() for artist in self.artists \
if artist.__class__.__name__ == 'Line2D'][0]
@linestyle.setter
def linestyle(self, val):
for artist in self.artists:
if artist.__class__.__name__ == 'Line2D':
artist.set_linestyle(val)
@property
def linewidth(self):
"""(int): The line width of the edge"""
return [artist.get_linewidth() for artist in self.artists \
if artist.__class__.__name__ == 'Line2D'][0]
@linewidth.setter
def linewidth(self, val):
for artist in self.artists:
if artist.__class__.__name__ == 'Line2D':
artist.set_linewidth(val)
def _create(self, x, y, ax, flipped, frame):
refs = sky_polygon(x, y, ax, flipped=flipped, frame=frame,
face_color=self._face_color,
face_alpha=self._face_alpha,
edge_color=self._edge_color,
edge_alpha=self._edge_alpha,
**self._kwargs)
return refs
def __repr__(self):
spaces = ' '*13
s = '<SkyPolygon: face_color={};\n'.format(self.face_color)
s += "{0}face_alpha={1};\n".format(spaces, self.face_alpha)
s += "{0}edge_color={1};\n".format(spaces, self.edge_color)
s += "{0}edge_alpha={1};\n".format(spaces, self.edge_alpha)
s += "{0}linestyle='{1}';\n".format(spaces, self.linestyle)
s += '{0}linewidth={1}>'.format(spaces, self.linewidth)
return s
[docs]class SkyAnnulus(PlotElement):
"""Plot an annulus on the sky in equatorial, galactic, or spacecraft
coordinates.
Parameters:
x (float): The azimuthal center, in degrees
y (float): The polar center, in degrees
radius (float): The radius in degrees, defined as the angular distance
from the center to the middle of the width of the
annulus band.
width (float): The width onf the annulus in degrees
ax (:class:`matplotlib.axes`): The axis on which to plot
flipped (bool, optional): If True, the azimuthal axis is flipped,
following equatorial convention
frame (str, optional): Either 'equatorial', 'galactic', 'spacecraft'.
Default is 'equatorial'
color (str, optional): The color of the annulus.
alpha (float, optional): The opacity of the annulus.
**kwargs: Other plotting options
"""
def __init__(self, x, y, radius, width, ax, flipped=True,
frame='equatorial', color='C0', alpha=1.0, **kwargs):
super().__init__(color=color, alpha=alpha)
self._kwargs = kwargs
artists = self._create(x, y, radius, width, ax, flipped, frame)
self._artists = self._sanitize_artists(artists)
@property
def color(self):
"""(str): The color of the annulus"""
return self._color
@color.setter
def color(self, color):
edge = colorConverter.to_rgba(color, alpha=1.0)
face = colorConverter.to_rgba(color, alpha=self.alpha)
[artist.set_edgecolor(edge) for artist in self._artists]
[artist.set_facecolor(face) for artist in self._artists]
self._color = color
@property
def hatch(self):
"""(str): The hatching string"""
return self._artists[0].get_hatch()
@hatch.setter
def hatch(self, val):
for artist in self._artists:
artist.set_hatch(val)
@property
def linestyle(self):
"""(str): The linestyle of the edge"""
return self._artists[0].get_linestyle()
@linestyle.setter
def linestyle(self, val):
for artist in self._artists:
artist.set_linestyle(val)
@property
def linewidth(self):
"""(int): The line width of the edge"""
return self._artists[0].get_linewidth()
@linewidth.setter
def linewidth(self, val):
for artist in self._artists:
artist.set_linewidth(val)
def _create(self, x, y, radius, width, ax, flipped, frame):
refs = sky_annulus(x, y, radius, width, ax, flipped=flipped,
frame=frame, color=self._color, alpha=self._alpha,
**self._kwargs)
return refs
def __repr__(self):
spaces = ' '*13
s = '<SkyAnnulus: color={};\n'.format(self.color)
s += "{0}alpha={1};\n".format(spaces, self.alpha)
s += "{0}linestyle='{1}';\n".format(spaces, self.linestyle)
s += '{0}linewidth={1}>'.format(spaces, self.linewidth)
return s
[docs]class Sun(PlotElement):
"""Plot the sun on the sky in equatorial, galactic, or spacecraft
coordinates
Parameters:
x (float): The azimuthal coordinate, in degrees
y (float): The polar coordinate, in degrees
ax (:class:`matplotlib.axes`): The axis on which to plot
flipped (bool, optional): If True, the azimuthal axis is flipped,
following equatorial convention
frame (str, optional): Either 'equatorial', 'galactic', 'spacecraft'.
Default is 'equatorial'
alpha (float, optional): The opacity of the annulus.
**kwargs: Other plotting options
"""
def __init__(self, x, y, ax, flipped=True, frame='equatorial', alpha=1.0,
**kwargs):
super().__init__(color=None, alpha=alpha)
self._kwargs = kwargs
artists = self._create(x, y, ax, flipped, frame)
self._artists = self._sanitize_artists(artists)
@property
def size(self):
"""(float): The size of the plot point"""
return self.artists[0].get_sizes()[0]
@size.setter
def size(self, val):
frac = val/self.size
self.artists[0].set_sizes([val])
face_size = self.artists[1].get_sizes()[0]
self.artists[1].set_sizes([frac * face_size])
def _create(self, x, y, ax, flipped, frame):
# :)
marker1 = "$\u263c$"
marker2 = "$\u263b$"
edge = colorConverter.to_rgba('gold', alpha=self._alpha)
face = colorConverter.to_rgba('yellow', alpha=0.75 * self._alpha)
point1 = sky_point(x, y, ax, marker=marker1, s=150.0, facecolor=face,
edgecolor=edge, flipped=flipped, frame=frame,
**self._kwargs)
point2 = sky_point(x, y, ax, marker=marker2, s=75.0, facecolor=face,
edgecolor=edge, flipped=flipped, frame=frame,
**self._kwargs)
return [point1, point2]
def __repr__(self):
s = '<Sun: alpha={};\n'.format(self.alpha)
s += ' size={}>'.format(self.size)
return s
[docs]class DetectorPointing(SkyCircle):
"""Plot a detector pointing on the sky in equatorial, galactic, or
spacecraft coordinates.
Parameters:
x (float): The azimuthal coordinate, in degrees
y (float): The polar coordinate, in degrees
radius (float): The radius of the circle, in degrees
ax (:class:`matplotlib.axes`): The axis on which to plot
flipped (bool, optional): If True, the azimuthal axis is flipped,
following equatorial convention
frame (str, optional): Either 'equatorial', 'galactic', 'spacecraft'.
Default is 'equatorial'
face_color (str, optional): The color of the circle fill
face_alpha (float, optional): The alpha opacity of the circle fill
edge_color (str, optional): The color of the circle edge
edge_alpha (float, optional): The alpha opacity of the circle edge
color (str, optional): The color of the circle. If set, overrides
``face_color`` and ``edge_color``.
alpha (float, optional): The alpha of the circle. If set, overrides
``face_alpha`` and ``edge_alpha``.
fontsize (float, optional): The size of the detector label
font_alpha (float, optional): The alpha opacity of the detector label
**kwargs: Other plotting options
"""
def __init__(self, x, y, radius, det, ax, flipped=True, frame='equatorial',
color='dimgray', alpha=None, edge_alpha=0.5, face_alpha=0.25,
fontsize=10, font_alpha=0.8, font_color=None, **kwargs):
super().__init__(x, y, radius, ax, color=color, alpha=alpha,
face_alpha=face_alpha, edge_alpha=edge_alpha,
frame=frame)
self._det = det
self._fontsize = fontsize
self._fontalpha = font_alpha
self._font_color = font_color
if font_color is None:
self._font_color = self._color
self._annotate(x, y, ax, flipped, frame)
@property
def font_alpha(self):
"""(float): The alpha opacity of the detector label"""
return self._fontalpha
@font_alpha.setter
def font_alpha(self, alpha):
self._artists[-1].set_alpha(alpha)
self._fontalpha = alpha
@property
def font_color(self):
"""(float): The color of the detector label"""
return self._font_color
@font_color.setter
def font_color(self, color):
self._artists[-1].set_color(color)
self._font_color = color
@property
def fontsize(self):
"""(float): The size of the detector label"""
return self._fontsize
@fontsize.setter
def fontsize(self, size):
self._artists[-1].set_fontsize(size)
self._fontsize = size
def _annotate(self, x, y, ax, flipped, frame):
# Need to demote (1,) arrays into scalars
x = x[0] if isinstance(x, np.ndarray) and x.shape == (1,) else x
y = y[0] if isinstance(y, np.ndarray) and y.shape == (1,) else y
theta = np.deg2rad(y)
phi = np.deg2rad(180.0 - x)
if frame == 'spacecraft':
phi -= np.pi
if phi < -np.pi:
phi += 2 * np.pi
elif frame == 'galactic':
phi -= np.pi
if phi < -np.pi:
phi += 2 * np.pi
else:
pass
if not flipped:
phi *= -1.0
txt = ax.text(phi, theta, self._det, fontsize=self._fontsize,
ha='center', va='center', color=self._font_color,
alpha=self._fontalpha, **self._kwargs)
self._artists.append(txt)
def __repr__(self):
spaces = ' '*19
s = "<DetectorPointing: '{}';\n".format(self._det)
s += '{0}face_color={1};\n'.format(spaces, self.face_color)
s += "{0}face_alpha={1};\n".format(spaces, self.face_alpha)
s += "{0}edge_color={1};\n".format(spaces, self.edge_color)
s += "{0}edge_alpha={1};\n".format(spaces, self.edge_alpha)
s += "{0}linestyle='{1}';\n".format(spaces, self.linestyle)
s += '{0}linewidth={1};\n'.format(spaces, self.linewidth)
s += '{0}fontsize={1};\n'.format(spaces, self.fontsize)
s += '{0}font_color={1};\n'.format(spaces, self.font_color)
s += '{0}font_alpha={1}>'.format(spaces, self.font_alpha)
return s
[docs]class GalacticPlane(PlotElement):
"""Plot the Galactic Plane in equatorial, galactic, or spacecraft
coordinates.
Parameters:
ax (:class:`matplotlib.axes`): The axis on which to plot
flipped (bool, optional): If True, the azimuthal axis is flipped,
following equatorial convention
frame (str or :class:`~gdt.core.coords.SpacecraftFrame`, optional):
If a string, then can either be 'equatorial' or 'galactic'.
Otherwise, it is the spacecraft frame definition. Defaults is
'equatorial'.
inner_color (str, optional): The color of the inner line element
outer_color (str, optional): The color of the outer line element
line_alpha (float, optional): The alpha opacity of the line elements of
the Galactic Plane
center_alpha (float, optional): The alpha opacity of the Galactic center
color (str, optional): The color of the Galactic plane. Overrides
``inner_color`` and ``outer_color``.
alpha (float, optional): The opacity of the Galactic plane. Overrides
``line_alpha`` and ``center_alpha``.
**kwargs: Other plotting options
"""
def __init__(self, ax, flipped=True, frame='equatorial', color=None,
inner_color='black', outer_color='dimgray', alpha=None,
line_alpha=0.5, center_alpha=0.75, **kwargs):
super().__init__(color=color, alpha=alpha)
self._kwargs = kwargs
# color and alpha act as setting the global color values for the object
if color is not None:
inner_color = color
outer_color = color
if alpha is not None:
line_alpha = alpha
center_alpha = alpha
self._line_alpha = line_alpha
self._center_alpha = center_alpha
self._inner_color = inner_color
self._outer_color = outer_color
artists = self._create(ax, flipped, frame)
self._artists = self._sanitize_artists(artists)
@property
def alpha(self):
"""(float): The alpha opacity value"""
return self._alpha
@alpha.setter
def alpha(self, alpha):
self.line_alpha = alpha
self.center_alpha = alpha
self._alpha = alpha
@property
def center_alpha(self):
"""(float): The alpha opacity of the Galactic plane"""
return self._center_alpha
@center_alpha.setter
def center_alpha(self, alpha):
[artist.set_alpha(alpha) for artist in self._artists \
if artist.__class__.__name__ == 'PathCollection']
self._center_alpha = alpha
@property
def color(self):
"""(str): The color of the Galactic plane"""
return self._color
@color.setter
def color(self, color):
self.inner_color = color
self.outer_color = color
self._color = color
@property
def inner_color(self):
"""(str): The color of the inner line element"""
return self._inner_color
@inner_color.setter
def inner_color(self, color):
[artist.set_color(color) for artist in self._artists \
if artist.__class__.__name__ == 'Line2D' and \
artist.get_color() == self._inner_color]
self._artists[-1].set_facecolor(color)
self._inner_color = color
@property
def line_alpha(self):
"""(float): The alpha opacity of the line elements"""
return self._line_alpha
@line_alpha.setter
def line_alpha(self, alpha):
[artist.set_alpha(alpha) for artist in self._artists \
if artist.__class__.__name__ == 'Line2D']
self._line_alpha = alpha
@property
def outer_color(self):
"""(str): The color of the outer line element"""
return self._outer_color
@outer_color.setter
def outer_color(self, color):
[artist.set_color(color) for artist in self._artists \
if artist.__class__.__name__ == 'Line2D' and \
artist.get_color() == self._outer_color]
self._artists[-2].set_facecolor(color)
self._outer_color = color
def _create(self, ax, flipped, frame):
refs = galactic_plane(ax, flipped=flipped, frame=frame,
outer_color=self._outer_color,
inner_color=self._inner_color,
line_alpha=self._line_alpha,
center_alpha=self._center_alpha, **self._kwargs)
return refs
def __repr__(self):
spaces = ' ' * 16
s = '<GalacticPlane: outer_color={};\n'.format(self.outer_color)
s += "{0}inner_color={1};\n".format(spaces, self.inner_color)
s += "{0}line_alpha={1};\n".format(spaces, self.line_alpha)
s += "{0}center_alpha={1};\n".format(spaces, self.center_alpha)
return s
[docs]class SkyHeatmap(PlotElement):
"""Plot a heatmap on the sky. By default, the heatmap opacity will scale
from 0 (fully transparent) to 1 (fully opaque) corresponding to the colormap
value, creating an alpha gradient. This behavior can be adjust by setting
``alpha_min`` and ``alpha_max``.
Parameters:
x (np.array):The azimuthal coordinate array of the heatmap grid
y (np.array): The polar coordinate array of the heatmap grid
heatmap (np.array): The heatmap array, of shape (``x.size``, ``y.size``)
ax (:class:`matplotlib.axes`): The axis on which to plot
color (str, optional): The colormap of the heatmap. Default is 'RdPu'
alpha_min (float, optional): The alpha opacity for the minimum color
value. Default is 0.
alpha_max (float, optional): The alpha opacity for the maximum color
value. Default is 1.
norm (:class:`matplotlib.colors.Normalize` or similar, optional):
The normalization used to scale the colormapping to the heatmap
values. This can be initialized by the defined matplotlib
normalizations or a custom normalization.
flipped (bool, optional): If True, the azimuthal axis is flipped,
following equatorial convention
frame (str, optional): Either 'equatorial', 'galactic', 'spacecraft'.
Default is 'equatorial'
**kwargs: Other plotting options
"""
def __init__(self, x, y, heatmap, ax, flipped=True, frame='equatorial',
color=GdtCmap('RdPu'), alpha=None, norm=None, **kwargs):
# set the normalization, and ensure that it is scaled to the data range
if norm is None:
norm = PowerNorm(gamma=0.3)
self._norm = norm
self._norm.vmin = heatmap.min()
self._norm.vmax = heatmap.max()
self._heatmap = heatmap
super().__init__(color=color, alpha=alpha)
self._kwargs = kwargs
artists = self._create(x, y, heatmap, ax, flipped, frame)
self._artists = self._sanitize_artists(artists)
# set the colormap
self.color = color
self.color.set_callback(self._artists[0].changed)
@property
def color(self):
"""(:class:`GdtCmap`): The colormap"""
return self._color
@color.setter
def color(self, color):
if not isinstance(color, GdtCmap):
raise TypeError('color must be of type GdtCmap')
self._color = color
self._artists[0].set_cmap(color.cmap)
@property
def norm(self):
"""(:class:matplotlib.colors.Normalize or similar):
The colormap normalization"""
return self._norm
@norm.setter
def norm(self, norm):
norm.vmin = self._heatmap.min()
norm.vmax = self._heatmap.max()
self._artists[0].set_norm(norm)
self._norm = norm
def _create(self, x, y, heatmap, ax, flipped, frame):
refs = sky_heatmap(x, y, heatmap, ax, cmap=self._color.cmap,
norm=self._norm, flipped=flipped, frame=frame,
**self._kwargs)
ax.grid(True)
return [refs]
def __repr__(self):
spaces = ' ' * 13
s = "<SkyHeatmap: color='{0}';\n{1}".format(self.color.name, spaces)
s += 'norm={0}>'.format(self.norm.__class__.__name__)
return s
[docs]class ModelData(PlotElement):
"""Plot a set of data with errors derived from the model variances.
Parameters:
x (np.array): The x coordinates of the data plot points
y (np.array): The y coordinates of the data plot points
xerr (np.array): The uncertainty of the x points
yerr (np.array): The uncertainty of the y points
ax (:class:`matplotlib.axes`): The axis on which to plot
ulmask (np.array(dtype=bool), optional):
A boolean array of the same size as x and y indicating which
points are upper limits (True) and which are data points (False).
color (str, optional): The color of the model
alpha (float, optional): The alpha of the fill. Default is 1
**kwargs: Other plotting options
"""
def __init__(self, x, y, xerr, yerr, ax, ulmask=None, color='C0',
alpha=1.0, **kwargs):
super().__init__(color=color, alpha=alpha)
self._kwargs = kwargs
artists = self._create(x, y, xerr, yerr, ulmask, ax)
self._artists = self._sanitize_artists(artists)
def _create(self, x, y, xerr, yerr, ulmask, ax):
return ax.errorbar(x, y, xerr=xerr, yerr=yerr, uplims=ulmask,
fmt='none',
color=self._color, alpha=self._alpha,
**self._kwargs)
def __repr__(self):
spaces = ' ' * 12
s = "<ModelData: color='{0}';\n{1}".format(self.color, spaces)
s += "alpha={0}>".format(self.alpha)
return s
[docs]class ModelSamples(PlotElement):
"""Plot a series of spectral model samples
Parameters:
x (np.array): The x coordinates of the model
y_list (list of np.array): A list of arrays, where each element is a
single spectral sample, having length equal
to x
ax (:class:`matplotlib.axes`): The axis on which to plot
color (str, optional): The color of the samples
alpha (float, optional): The alpha of the fill. Default is 1
label (str, optional): The label for the samples
**kwargs: Other plotting options
"""
def __init__(self, x, y_list, ax, color='C0', alpha=1.0, label=None,
**kwargs):
super().__init__(color=color, alpha=alpha)
self._kwargs = kwargs
artists = self._create(x, y_list, ax)
self._artists = self._sanitize_artists(artists)
if label is not None:
self._artists[0].set_label(label)
@property
def linestyle(self):
"""(str): The linestyle of the histogram. Default is '-'"""
return self._artists[0].get_linestyle()
@linestyle.setter
def linestyle(self, val):
for artist in self._artists:
artist.set_linestyle(val)
@property
def linewidth(self):
"""(int): The line width of the histogram. Default is 1.5"""
return self._artists[0].get_linewidth()
@linewidth.setter
def linewidth(self, val):
for artist in self._artists:
artist.set_linewidth(val)
def _create(self, x, y_list, ax):
return [ax.plot(x, y, color=self._color, alpha=self._alpha,
**self._kwargs) for y in y_list]
def __repr__(self):
spaces = ' ' * 15
s = "<ModelSamples: color='{}';\n".format(self.color)
s += "{0}alpha={1};\n{0}linestyle='{2}';\n".format(spaces, self.alpha,
self.linestyle)
s += '{0}linewidth={1}>'.format(spaces, self.linewidth)
return s
[docs]class PlotElementCollection(DataCollection):
"""A collection class for plot elements. This differs from DataCollection
in that plot elements can be accessed as attributes by their name.
For example:
>>> collection = PlotElementCollection()
>>> collection.include(my_obj, name='yeehaw')
>>> collection.yeehaw
<my_obj>
"""
def __getattr__(self, name):
return self.get_item(name)