from Ska.Matplotlib import plot_cxctime, pointpair, \
cxctime2plotdate
from matplotlib import font_manager
import matplotlib.pyplot as plt
from matplotlib.dates import num2date
from acispy.utils import ensure_list
from cxotime import CxoTime
from datetime import datetime
from collections import OrderedDict
from matplotlib.backends.backend_agg import \
FigureCanvasAgg
from io import BytesIO
from mpl_toolkits.axes_grid1 import make_axes_locatable
from acispy.utils import convert_state_code
import numpy as np
from astropy.units import Quantity
datefmt = "%Y-%m-%d %H:%M:%S.%f"
drawstyles = {"simpos": "steps",
"pitch": "steps",
"ccd_count": "steps",
"ccsdstmf": "steps"}
units_map = {"deg_C": "Temperature",
"deg_F": "Temperature",
"V": "Voltage",
"A": "Current",
"W": "Power",
"deg": 'Angle',
"deg**2": "Solid Angle"}
unit_labels = {"V": 'V',
"A": 'A',
"deg_C": '$\mathrm{^\circ{C}}$',
"deg_F": '$\mathrm{^\circ{F}}$',
"W": "W",
"s": "s",
"deg": "deg"}
class ACISPlot(object):
def __init__(self, fig, ax, lines, ax2, lines2):
self.fig = fig
self.ax = ax
self.legend = None
self.lines = lines
self.ax2 = ax2
self.lines2 = lines2
def _repr_png_(self):
canvas = FigureCanvasAgg(self.fig)
f = BytesIO()
canvas.print_figure(f)
f.seek(0)
return f.read()
def savefig(self, filename, **kwargs):
"""
Save the figure to the file specified by *filename*.
"""
self.fig.savefig(filename, **kwargs)
def set_title(self, label, fontsize=18, loc='center', **kwargs):
"""
Add a title to the top of the plot.
Parameters
----------
label : string
The title itself.
fontsize : integer, optional
The size of the font. Default: 18 pt
loc : string, optional
The horizontal location of the title. Options are: 'left',
'right', 'center'. Default: 'center'
Examples
--------
>>> p.set_title("my awesome plot", fontsize=15, loc='left')
"""
fontdict = {"size": fontsize}
self.ax.set_title(label, fontdict=fontdict, loc=loc, **kwargs)
def set_grid(self, on):
"""
Turn grid lines on or off on the plot.
Parameters
----------
on : boolean
Set to True to put the lines on, set to False to remove them.
"""
self.ax.grid(on)
def add_hline(self, y, lw=2, ls='-', color='green', **kwargs):
"""
Add a horizontal line on the y-axis of the plot.
Parameters
----------
y : float
The value to place the vertical line at.
lw : integer, optional
The width of the line. Default: 2
ls : string, optional
The style of the line. Can be one of:
'solid', 'dashed', 'dashdot', 'dotted'.
Default: 'solid'
color : string, optional
The color of the line. Default: 'green'
Examples
--------
>>> p.add_hline(36., lw=3, ls='dashed', color='red')
"""
self.ax.axhline(y=y, lw=lw, ls=ls, color=color,
label='_nolegend_', **kwargs)
def add_vline(self, x, lw=2, ls='-', color='green', **kwargs):
"""
Add a vertical line on the x-axis of the plot.
Parameters
----------
x : float
The value to place the vertical line at.
lw : integer, optional
The width of the line. Default: 2
ls : string, optional
The style of the line. Can be one of:
'solid', 'dashed', 'dashdot', 'dotted'.
Default: 'solid'
color : string, optional
The color of the line. Default: 'green'
Examples
--------
>>> p.add_vline(25., lw=3, ls='dashed', color='red')
"""
self.ax.axvline(x=x, lw=lw, ls=ls, color=color, **kwargs,
label='_nolegend_')
def set_ylim(self, ymin, ymax):
"""
Set the limits on the left y-axis of the plot to *ymin* and *ymax*.
"""
self.ax.set_ylim(ymin, ymax)
def set_ylabel(self, ylabel, fontsize=18, **kwargs):
"""
Set the label of the left y-axis of the plot.
Parameters
----------
ylabel : string
The new label.
fontsize : integer, optional
The size of the font. Default: 18 pt
Examples
--------
>>> pp.set_ylabel("DPA Temperature", fontsize=15)
"""
fontdict = {"size": fontsize}
self.ax.set_ylabel(ylabel, fontdict=fontdict, **kwargs)
def redraw(self):
"""
Re-draw the plot.
"""
self.fig.canvas.draw()
def tight_layout(self, *args, **kwargs):
self.fig.tight_layout(*args, **kwargs)
def get_figure(plot, fig, subplot, figsize):
ax2 = None
lines2 = []
if plot is None:
if fig is None:
fig = plt.figure(figsize=figsize)
if subplot is None:
subplot = 111
if not isinstance(subplot, tuple):
subplot = (subplot,)
ax = fig.add_subplot(*subplot)
lines = []
else:
fig = plot.fig
if subplot is None:
ax = plot.ax
else:
ax = fig.add_subplot(subplot)
lines = plot.lines
if hasattr(plot, "ax2"):
ax2 = plot.ax2
lines2 = plot.lines2
for axis in ['top', 'bottom', 'left', 'right']:
ax.spines[axis].set_linewidth(2)
return fig, ax, lines, ax2, lines2
[docs]class CustomDatePlot(ACISPlot):
r"""
Make a custom date vs. value plot.
Parameters
----------
dates : array of strings
The dates to be plotted.
values : array
The values to be plotted.
lw : float, optional
The width of the lines in the plots. Default: 2 px.
ls : string, optional
The line style of the line. Default: '-'
fontsize : integer, optional
The font size for the labels in the plot. Default: 18 pt.
figsize : tuple of integers, optional
The size of the plot in (width, height) in inches. Default: (10, 8)
plot : :class:`~acispy.plots.DatePlot` or :class:`~acispy.plots.CustomDatePlot`, optional
An existing DatePlot to add this plot to. Default: None, one
will be created if not provided.
"""
def __init__(self, dates, values, fmt='-b', lw=2, fontsize=18, ls='-',
figsize=(10, 8), color=None, plot=None, fig=None,
subplot=None, **kwargs):
fig, ax, lines, ax2, lines2 = get_figure(plot, fig, subplot, figsize)
dates = CxoTime(dates).secs
if len(dates.shape) == 2:
tstart, tstop = np.asarray(dates)
x = pointpair(tstart, tstop)
y = pointpair(np.asarray(values))
else:
x = np.asarray(dates)
y = np.asarray(values)
if color is None:
color = f"C{len(lines)}"
ticklocs, fig, ax = plot_cxctime(x, y, fmt=fmt, fig=fig, ax=ax,
lw=lw, ls=ls, color=color, **kwargs)
super(CustomDatePlot, self).__init__(fig, ax, lines, ax2, lines2)
self.ax.tick_params(which="major", width=2, length=6)
self.ax.tick_params(which="minor", width=2, length=3)
self.lines.append(ax.lines[-1])
self.ax.set_xlabel("Date", fontdict={"size": fontsize})
fontProperties = font_manager.FontProperties(size=fontsize)
for label in self.ax.get_xticklabels():
label.set_fontproperties(fontProperties)
for label in self.ax.get_yticklabels():
label.set_fontproperties(fontProperties)
self.times = dates
self.y = values
[docs] def plot_right(self, dates, values, fmt='-b', lw=2, fontsize=18,
ls='-', color="magenta", **kwargs):
"""
Plot a quantity on the right x-axis of this plot.
Parameters
----------
dates : array of strings
The dates to be plotted.
values : array
The values to be plotted.
lw : float, optional
The width of the lines in the plots. Default: 2 px.
ls : string, optional
The line style of the line. Default: '-'
fontsize : integer, optional
The font size for the labels in the plot. Default: 18 pt.
figsize : tuple of integers, optional
The size of the plot in (width, height) in inches. Default: (10, 8)
plot : :class:`~acispy.plots.DatePlot` or :class:`~acispy.plots.CustomDatePlot`, optional
An existing DatePlot to add this plot to. Default: None, one
will be created if not provided.
"""
dates = CxoTime(dates).secs
if self.ax2 is None:
self.ax2 = self.ax.twinx()
self.ax2.set_zorder(-10)
self.ax.patch.set_visible(False)
if len(dates.shape) == 2:
tstart, tstop = np.asarray(dates)
x = pointpair(tstart, tstop)
y = pointpair(np.asarray(values))
else:
x = np.asarray(dates)
y = np.asarray(values)
plot_cxctime(x, y, fmt=fmt, fig=self.fig,
ax=self.ax2, ls=ls, color=color, lw=lw, **kwargs)
self.ax2.tick_params(which="major", width=2, length=6)
self.ax2.tick_params(which="minor", width=2, length=3)
fontProperties = font_manager.FontProperties(size=fontsize)
for label in self.ax2.get_xticklabels():
label.set_fontproperties(fontProperties)
for label in self.ax2.get_yticklabels():
label.set_fontproperties(fontProperties)
[docs] def set_xlim(self, xmin, xmax):
"""
Set the limits on the x-axis of the plot to *xmin* and *xmax*,
which must be in YYYY:DOY:HH:MM:SS format.
Examples
--------
>>> p.set_xlim("2016:050:12:45:47.324", "2016:056:22:32:01.123")
"""
if not isinstance(xmin, datetime):
xmin = datetime.strptime(CxoTime(xmin).iso, datefmt)
if not isinstance(xmax, datetime):
xmax = datetime.strptime(CxoTime(xmax).iso, datefmt)
self.ax.set_xlim(xmin, xmax)
[docs] def add_hline(self, y, lw=2, ls='-', color='green',
xmin=None, xmax=None, **kwargs):
"""
Add a horizontal line on the y-axis of the plot.
Parameters
----------
y : float
The value to place the vertical line at.
lw : integer, optional
The width of the line. Default: 2
ls : string, optional
The style of the line. Can be one of:
'solid', 'dashed', 'dashdot', 'dotted'.
Default: 'solid'
color : string, optional
The color of the line. Default: 'green'
Examples
--------
>>> p.add_hline(36., lw=3, ls='dashed', color='red')
"""
if xmin is None:
xmin = 0
else:
xmin = datetime.strptime(CxoTime(xmin).iso, datefmt)
if xmax is None:
xmax = 1
else:
xmax = datetime.strptime(CxoTime(xmax).iso, datefmt)
self.ax.axhline(y=y, lw=lw, ls=ls, color=color, xmin=xmin,
xmax=xmax, label='_nolegend_', **kwargs)
[docs] def add_vline(self, time, lw=2, ls='solid', color='green', **kwargs):
"""
Add a vertical line on the time axis of the plot.
Parameters
----------
time : string
The time to place the vertical line at.
Must be in YYYY:DOY:HH:MM:SS format.
lw : integer, optional
The width of the line. Default: 2
ls : string, optional
The style of the line. Can be one of:
'solid', 'dashed', 'dashdot', 'dotted'.
Default: 'solid'
color : string, optional
The color of the line. Default: 'green'
Examples
--------
>>> p.add_vline("2016:101:12:36:10.102", lw=3, ls='dashed', color='red')
"""
time = datetime.strptime(CxoTime(time).iso, datefmt)
self.ax.axvline(x=time, lw=lw, ls=ls, color=color, **kwargs,
label='_nolegend_')
[docs] def add_text(self, time, y, text, fontsize=18, color='black',
rotation='horizontal', **kwargs):
"""
Add text to a DatePlot.
Parameters
----------
time : string
The time to place the text at.
Must be in YYYY:DOY:HH:MM:SS format.
y : float
The y-value to place the text at.
text : string
The text itself.
fontsize : integer, optional
The size of the font. Default: 18 pt.
color : string, optional
The color of the font. Default: black.
rotation : string or float, optional
The rotation of the text. Default: Horizontal
Examples
--------
>>> dp.add_text("2016:101:12:36:10.102", 35., "Something happened here!",
... fontsize=15, color='magenta')
"""
time = datetime.strptime(CxoTime(time).iso, datefmt)
self.ax.text(time, y, text, fontsize=fontsize, color=color,
rotation=rotation, **kwargs)
[docs] def set_line_label(self, line, label):
"""
Change the field label in the legend.
Parameters
----------
line : integer
The line whose label to change given by its number, assuming
a starting index of 0.
label :
The label to set it to.
Examples
--------
>>> dp.set_line_label(1, "DEA Temperature")
"""
self.lines[line].set_label(label)
self.set_legend()
[docs] def set_legend(self, loc='best', fontsize=16, zorder=None, **kwargs):
"""
Adjust a legend on the plot.
Parameters
----------
loc : string, optional
The location of the legend on the plot. Options are:
'best'
'upper right'
'upper left'
'lower left'
'lower right'
'right'
'center left'
'center right'
'lower center'
'upper center'
'center'
Default: 'best', which will try to find the best location for
the legend, e.g. away from plotted data.
fontsize : integer, optional
The size of the legend text. Default: 16 pt.
Examples
--------
>>> p.set_legend(loc='right', fontsize=18)
"""
prop = {"size": fontsize}
self.legend = self.ax.legend(loc=loc, prop=prop, **kwargs)
if zorder is not None:
self.legend.set_zorder(zorder)
[docs] def fill_between(self, datestart, datestop, color, alpha=1.0):
"""
Fill a shaded region between two times on the plot.
Parameters
----------
datestart : string
The beginning time of the shaded region. Must be in the
YYYY:DOY:HH:MM:SS format.
datestop : string
The ending time of the shaded region. Must be in the
YYYY:DOY:HH:MM:SS format.
color : string
The color of the shaded region.
alpha : float, optional
The transparency of the shaded region. Default is 1.0,
which is opaque.
"""
tmin, tmax = self.ax.get_xlim()
ybot, ytop = self.ax.get_ylim()
t = np.linspace(tmin, tmax, 1000)
tbegin, tend = cxctime2plotdate(CxoTime([datestart, datestop]).secs)
where = (t >= tbegin) & (t <= tend)
self.ax.fill_between(t, ybot, ytop, where=where,
color=color, alpha=alpha)
[docs] def annotate_obsids(self, ypos, ds=None, show_manuvrs=False, ywidth=2.0,
txtheight=1.0, lw=2.0, fontsize=16, datestart=None,
datestop=None, color='red', manuvr_color='blue',
txtloc=0.5):
"""
Annotate obsids on a :class:`~acispy.plots.CustomDatePlot` or
:class:`~acispy.plots.DatePlot` instance. They will be annotated
using lines to mark the beginning and end of obsids, and the
actual obsid numbers will be marked on the plot.
Parameters
----------
ypos : float
The location on the y-axis at which the obsid lines will be
annotated.
ds : :class:`~acispy.dataset.Dataset`, optional
If this is a :class:`~acispy.plots.CustomDatePlot` object,
you will need to pass in a Dataset object to obtain the
obsid information. Default: None
show_manuvrs : boolean, optional
If True, obsid changes associated with maneuvers will
be shown. Default: False
ywidth : float, optional
The height of the lines marking obsid changes.
txtheight : float, optional
The height in points where to place the text above the obsids
line.
lw : float, optional
The linewidth of the lines. Default: 2.0
fontsize : integer, optional
The font size of the text.
datestart : string, optional
Only show obsids after this date and time. Must be in the
YYYY:DOY:HH:MM:SS format. Default is to show from the
beginning of the plot.
datestop : string, optional
Only show obsids before this date and time. Must be in the
YYYY:DOY:HH:MM:SS format. Default is to show up to the
end of the plot.
color : string, optional
The color of the lines. Default: 'red'
manuvr_color : string, optional
The color of the lines for maneuver obsids. Default: 'blue'
txtloc : float, optional
A float between 0 and 1 to mark the location between
the start and stop of the obsid where the obsid number is
annotated.
"""
tmin, tmax = self.ax.get_xlim()
if datestart is not None:
tmin = cxctime2plotdate(CxoTime(datestart).secs)
if datestop is not None:
tmax = cxctime2plotdate(CxoTime(datestop).secs)
if ds is None:
ds = getattr(self, "ds", None)
states = getattr(ds, "states", None)
if states is None:
raise RuntimeError("The dataset associated with or provided to this plot "
"does not include commanded states! Provide a dataset "
"using the keyword argument 'ds' to 'annotate_obsids'!")
idxs = np.char.count(states["trans_keys"].value, "obsid").astype("bool")
obsids = np.insert(states["obsid"][idxs].value, 0, states["obsid"][0].value)
tstart = cxctime2plotdate(np.insert(states["obsid"].times[0, idxs].value,
0, states["obsid"].times[0, 0].value))
endstop = cxctime2plotdate([states["obsid"].times[1, -1].value])
tstop = np.concatenate([tstart[:-1]+np.diff(tstart), endstop])
endcapstart = ypos-0.5*ywidth
endcapstop = ypos+0.5*ywidth
textypos = ypos+txtheight
num_obsids = obsids.size
for i, (ti, tf, obsid) in enumerate(zip(tstart, tstop, obsids)):
clr = color if obsid <= 40000 else manuvr_color
if obsid <= 40000 or show_manuvrs:
self.ax.hlines(ypos, ti, tf, linestyle='-',
color=clr, lw=lw)
if i > 0:
self.ax.vlines(ti, endcapstart, endcapstop,
color=clr, lw=lw, zorder=100)
if i < num_obsids-1:
self.ax.vlines(tf, endcapstart, endcapstop,
color=clr, lw=lw, zorder=100)
tmid = ti + txtloc*(tf - ti)
if tmin <= tmid <= tmax:
self.ax.text(tmid, textypos, obsid, color=clr,
rotation=90, va='bottom', fontsize=fontsize)
[docs]class DatePlot(CustomDatePlot):
r""" Make a single-panel plot of a quantity (or multiple quantities)
vs. date and time.
Multiple quantities can be plotted on the left
y-axis together if they have the same units, otherwise a quantity
with different units can be plotted on the right y-axis.
Parameters
----------
ds : :class:`~acispy.dataset.Dataset`
The Dataset instance to get the data to plot from.
fields : tuple of strings or list of tuples of strings
A single field or list of fields to plot on the left y-axis.
field2 : tuple of strings, optional
A single field to plot on the right y-axis. Default: None
lw : float or list of floats, optional
The width of the lines in the plots. If a list, the length
of a the list must be equal to the number of fields. If a
single number, it will apply to all plots. Default: 2 px.
ls : string, optional
The line style of the lines plotted on the left y-axis.
Can be a single linestyle or more than one for each line.
Default: '-'
ls2 : string, optional
The line style of the line plotted on the right y-axis.
Can be a single linestyle or more than one for each line.
Default: '-'
lw2 : float, optional
The width of the line plotted on the right y-axis.
fontsize : integer, optional
The font size for the labels in the plot. Default: 18 pt.
color : list of strings, optional
The colors for the lines plotted on the left y-axis. Can
be a single color or more than one in a list. Default:
Use the default Matplotlib order of colors.
color2 : string, optional
The color for the line plotted on the right y-axis.
Default: "magenta"
fig : :class:`~matplotlib.figure.Figure`, optional
A Figure instance to plot in. Default: None, one will be
created if not provided.
figsize : tuple of integers, optional
The size of the plot in (width, height) in inches. Default: (10, 8)
plot : :class:`~acispy.plots.DatePlot` or :class:`~acispy.plots.CustomDatePlot`, optional
An existing DatePlot to add this plot to. Default: None, one
will be created if not provided.
plot_bad : boolean, optional
If True, "bad" values will be plotted but the ranges of bad values
will be marked with translucent blue rectangles. If False, bad
values will be removed from the plot. Default: False
Examples
--------
>>> from acispy import DatePlot
>>> p1 = DatePlot(ds, ("msids", "1dpamzt"), field2=("states", "pitch"),
... lw=2, color="brown")
>>> from acispy import DatePlot
>>> fields = [("msids", "1dpamzt"), ("msids", "1deamzt"), ("msids", "1pdeaat")]
>>> p2 = DatePlot(ds, fields, fontsize=12, color=["brown","black","orange"])
"""
def __init__(self, ds, fields, field2=None, fmt='-b', lw=2, ls='-',
ls2='-', lw2=2, fontsize=18, color=None, color2='magenta',
figsize=(10, 8), plot=None, fig=None, subplot=None,
plot_bad=False):
fig, ax, lines, ax2, lines2 = get_figure(plot, fig, subplot, figsize)
super(CustomDatePlot, self).__init__(fig, ax, lines, ax2, lines2)
fields = ensure_list(fields)
lw = ensure_list(lw)
self.num_fields = len(fields)
if len(lw) == 1 and len(fields) > 1:
lw = lw*self.num_fields
if color is None:
color = [None]*len(fields)
color = ensure_list(color)
ls = ensure_list(ls)
if len(ls) != len(fields):
if len(ls) == 1:
ls = ls*len(fields)
else:
raise RuntimeError("The number of linestyles must equal the number "
"of fields or must be only one style!")
self.times = {}
self.y = {}
self.fields = []
for i, field in enumerate(fields):
field = ds._determine_field(field)
self.fields.append(field)
src_name, fd = field
drawstyle = drawstyles.get(fd, None)
state_codes = ds.state_codes.get(field, None)
if not plot_bad:
mask = ds[field].mask
else:
mask = slice(None, None, None)
if state_codes is None:
y = ds[field].value[mask]
else:
state_codes = [(v, k) for k, v in state_codes.items()]
y = convert_state_code(ds, field)[mask]
if src_name == "states":
tstart, tstop = ds[field].times
x = pointpair(tstart.value[mask], tstop.value[mask])
y = pointpair(y[mask])
else:
x = ds[field].times.value[mask]
label = ds.fields[field].display_name
_, fig, ax = plot_cxctime(x, y, fmt=fmt, fig=fig, lw=lw[i],
ax=ax, color=color[i], ls=ls[i],
state_codes=state_codes,
drawstyle=drawstyle,
label=label)
self.lines.append(ax.lines[-1])
self.y[field] = ds[field][mask]
self.times[field] = ds[field][mask].times
self.fig = fig
self.ax = ax
self.ds = ds
self.ax.set_xlabel("Date", fontdict={"size": fontsize})
if self.num_fields > 1:
self.ax.legend(loc=0)
fontProperties = font_manager.FontProperties(size=fontsize)
for label in self.ax.get_xticklabels():
label.set_fontproperties(fontProperties)
for label in self.ax.get_yticklabels():
label.set_fontproperties(fontProperties)
ymin, ymax = self.ax.get_ylim()
if ymin > 0:
ymin *= 0.95
else:
ymin *= 1.05
if ymax > 0:
ymax *= 1.05
else:
ymax *= 0.95
self.ax.set_ylim(ymin, ymax)
if self.num_fields > 0:
units = ds.fields[self.fields[0]].units
ulabel = unit_labels.get(units, units)
if self.num_fields > 1:
if units == '':
ylabel = ''
else:
ylabel = f"{units_map[units]} ({ulabel})"
self.set_ylabel(ylabel)
else:
ylabel = ds.fields[self.fields[0]].display_name
if units != '':
ylabel += f" ({ulabel})"
self.set_ylabel(ylabel)
self.ax.tick_params(which="major", width=2, length=6)
self.ax.tick_params(which="minor", width=2, length=3)
if field2 is not None:
field2 = ds._determine_field(field2)
self.field2 = field2
src_name2, fd2 = field2
if self.ax2 is None:
self.ax2 = self.ax.twinx()
self.ax2.set_zorder(-10)
self.ax.patch.set_visible(False)
self.ax2.tick_params(which="major", width=2, length=6)
self.ax2.tick_params(which="minor", width=2, length=3)
drawstyle = drawstyles.get(fd2, None)
state_codes = ds.state_codes.get(field2, None)
if not plot_bad:
mask2 = ds[field2].mask
else:
mask2 = slice(None, None, None)
if state_codes is None:
y2 = ds[field2].value[mask2]
else:
state_codes = [(v, k) for k, v in state_codes.items()]
y2 = convert_state_code(ds, field2)[mask2]
if src_name2 == "states":
tstart, tstop = ds[field2].times
x2 = pointpair(tstart.value[mask2], tstop.value[mask2])
y2 = pointpair(y2[mask2])
else:
x2 = ds[field2].times.value[mask2]
plot_cxctime(x2, y2, fig=fig, ax=self.ax2, ls=ls2,
lw=lw2, drawstyle=drawstyle, color=color2,
state_codes=state_codes)
self.lines2.append(self.ax2.lines[-1])
self.times[field2] = ds[field2][mask2].times
self.y[field2] = ds[field2][mask2]
for label in self.ax2.get_xticklabels():
label.set_fontproperties(fontProperties)
for label in self.ax2.get_yticklabels():
label.set_fontproperties(fontProperties)
ymin2, ymax2 = self.ax2.get_ylim()
if ymin2 > 0:
ymin2 *= 0.95
else:
ymin2 *= 1.05
if ymax2 > 0:
ymax2 *= 1.05
else:
ymax2 *= 0.95
self.ax2.set_ylim(ymin2, ymax2)
units2 = ds.fields[field2].units
ylabel2 = ds.fields[field2].display_name
ulabel2 = unit_labels.get(units2, units2)
if units2 != '':
ylabel2 += f" ({ulabel2})"
self.set_ylabel2(ylabel2)
else:
self.field2 = None
if plot_bad:
self._fill_bad_times()
def _fill_bad_times(self):
masks = []
times = []
for field in self.fields:
if field[0] != "states":
times.append(self.times[field])
masks.append(self.y[field].mask)
axes = [self.ax]*len(times)
if self.field2 and self.field2[0] != "states":
axes.append(self.ax2)
times.append(self.times[self.field2])
masks.append(self.y[self.field2].mask)
for mask, ax, x in zip(masks, axes, times):
if np.any(~mask):
ybot, ytop = ax.get_ylim()
all_time = cxctime2plotdate(x.value)
bad = np.concatenate([[False], ~mask, [False]])
bad_int = np.flatnonzero(bad[1:] != bad[:-1]).reshape(-1, 2)
for ii, jj in bad_int:
ax.fill_between(all_time[ii:jj], ybot, ytop,
where=~mask[ii:jj], color='cyan', alpha=0.5)
[docs] def set_ylim(self, ymin, ymax):
"""
Set the limits on the left y-axis of the plot to *ymin* and *ymax*.
"""
self.ax.set_ylim(ymin, ymax)
self._fill_bad_times()
[docs] def set_ylim2(self, ymin, ymax):
"""
Set the limits on the right y-axis of the plot to *ymin* and *ymax*.
"""
self.ax2.set_ylim(ymin, ymax)
self._fill_bad_times()
[docs] def set_ylabel2(self, ylabel, fontsize=18, **kwargs):
"""
Set the label of the right y-axis of the plot.
Parameters
----------
ylabel : string
The new label.
fontsize : integer, optional
The size of the font. Default: 18 pt
Examples
--------
>>> p1.set_ylabel2("Pitch Angle in Degrees", fontsize=14)
"""
fontdict = {"size": fontsize}
self.ax2.set_ylabel(ylabel, fontdict=fontdict, **kwargs)
[docs] def add_hline2(self, y2, lw=2, ls='solid', color='green', **kwargs):
"""
Add a horizontal line on the right y-axis of the plot.
Parameters
----------
y2 : float
The value to place the vertical line at.
lw : integer, optional
The width of the line. Default: 2
ls : string, optional
The style of the line. Can be one of:
'solid', 'dashed', 'dashdot', 'dotted'.
Default: 'solid'
color : string, optional
The color of the line. Default: 'green'
Examples
--------
>>> p.add_hline2(105., lw=3, ls='dashed', color='red')
"""
self.ax2.axhline(y=y2, lw=lw, ls=ls, color=color, **kwargs,
label='_nolegend_')
[docs] def set_field_label(self, field, label):
"""
Change the field label in the legend.
Parameters
----------
field : (type, name) tuple
The field whose label to change.
label :
The label to set it to.
Examples
--------
>>> dp.set_field_label(("msids","1deamzt"), "DEA Temperature")
"""
fd = self.ds._determine_field(field)
idx = self.fields.index(fd)
self.set_line_label(idx, label)
class DummyDatePlot(object):
def __init__(self, fig, ax):
self.fig = fig
self.ax = ax
self.lines = []
def make_dateplots(*args, **kwargs):
fig, axes = plt.subplots(*args, **kwargs)
if not hasattr(axes, "shape"):
return DummyDatePlot(fig, axes)
elif len(axes.shape) == 1:
return np.array([DummyDatePlot(fig, ax) for ax in axes])
elif len(axes.shape) == 2:
plots = np.empty(axes.shape, dtype=DummyDatePlot)
nx, ny = axes.shape
for i in range(nx):
for j in range(ny):
plots[i][j] = DummyDatePlot(fig, axes[i][j])
return plots
[docs]class MultiDatePlot(object):
r""" Make a multi-panel plot of multiple quantities vs. date and time.
Parameters
----------
ds : :class:`~acispy.dataset.Dataset`
The Dataset instance to get the data to plot from.
fields : list of tuples of strings
A list of fields to plot.
subplots : tuple of integers, optional
The gridded layout of the plots, i.e. (num_x_plots, num_y_plots)
The default is to have all plots stacked vertically.
fontsize : integer, optional
The font size for the labels in the plot. Default: 15 pt.
lw : float, optional
The width of the lines in the plots. Default: 2 px.
color : string, optional
The color of the lines in the plots. Can be a single color or
one color for each plot. Default is to use a single color, which
is the Matplotlib default.
figsize : tuple of integers, optional
The size of the plot in (width, height) in inches. Default: (12, 12)
plot_bad : boolean, optional
If True, "bad" values will be plotted but the ranges of bad values
will be marked with translucent blue rectangles. If False, bad
values will be removed from the plot. Default: False
Examples
--------
>>> from acispy import MultiDatePlot
>>> fields = [("msids", "1deamzt"), ("model", "1deamzt"), ("states", "ccd_count")]
>>> mp = MultiDatePlot(ds, fields, lw=2, subplots=(2, 2))
>>> from acispy import MultiDatePlot
>>> fields = [[("msids", "1deamzt"), ("model", "1deamzt")], ("states", "ccd_count")]
>>> mp = MultiDatePlot(ds, fields, lw=2)
"""
def __init__(self, ds, fields, subplots=None,
fontsize=15, lw=2, figsize=(12, 12),
color=None, plot_bad=False):
fig = plt.figure(figsize=figsize)
if subplots is None:
subplots = len(fields), 1
self.plots = OrderedDict()
if color is None:
color = [None]*len(fields)
color = ensure_list(color)
for i, field in enumerate(fields):
ax = fig.add_subplot(subplots[0], subplots[1], i+1)
ddp = DummyDatePlot(fig, ax)
if isinstance(field, list):
fd = field[0]
else:
fd = field
# This next line is to raise an error if we have
# multiple field types with the same name
ds._determine_field(fd)
self.plots[fd] = DatePlot(ds, field, plot=ddp, lw=lw,
color=color[i], plot_bad=plot_bad)
ax.xaxis.label.set_size(fontsize)
ax.yaxis.label.set_size(fontsize)
ax.xaxis.set_tick_params(labelsize=fontsize)
ax.yaxis.set_tick_params(labelsize=fontsize)
self.fig = fig
xmin, xmax = self.plots[list(self.plots.keys())[0]].ax.get_xlim()
self.set_xlim(num2date(xmin), num2date(xmax))
def __getitem__(self, item):
return self.plots[item]
[docs] def set_xlim(self, xmin, xmax):
"""
Set the limits on the x-axis of the plot to *xmin* and *xmax*,
which must be in YYYY:DOY:HH:MM:SS format.
"""
for plot in self.plots.values():
plot.set_xlim(xmin, xmax)
[docs] def add_vline(self, x, lw=2, ls='-', color='green', **kwargs):
"""
Add a vertical line on the time axis of the plot.
Parameters
----------
time : string
The time to place the vertical line at.
Must be in YYYY:DOY:HH:MM:SS format.
lw : integer, optional
The width of the line. Default: 2
ls : string, optional
The style of the line. Can be one of:
'solid', 'dashed', 'dashdot', 'dotted'.
Default: 'solid'
color : string, optional
The color of the line. Default: 'green'
Examples
--------
>>> p.add_vline("2016:101:12:36:10.102", lw=3, ls='dashed', color='red')
"""
for plot in self.plots.values():
plot.add_vline(x, lw=lw, ls=ls, color=color, **kwargs)
[docs] def set_title(self, label, fontsize=18, loc='center', **kwargs):
"""
Add a title to the top of the plot.
Parameters
----------
label : string
The title itself.
fontsize : integer, optional
The size of the font. Default: 18 pt
loc : string, optional
The horizontal location of the title. Options are: 'left',
'right', 'center'. Default: 'center'
Examples
--------
>>> p.set_title("my awesome plot", fontsize=15, loc='left')
"""
list(self.plots.values())[0].set_title(label, fontsize=fontsize, loc=loc, **kwargs)
[docs] def set_grid(self, on):
"""
Turn grid lines on or off on the plot.
Parameters
----------
on : boolean
Set to True to put the lines on, set to False to remove them.
"""
for plot in self.plots.values():
plot.set_grid(on)
[docs] def savefig(self, filename, **kwargs):
"""
Save the figure to the file specified by *filename*.
"""
self.fig.savefig(filename, **kwargs)
def _repr_png_(self):
canvas = FigureCanvasAgg(self.fig)
f = BytesIO()
canvas.print_figure(f)
f.seek(0)
return f.read()
[docs] def redraw(self):
"""
Re-draw the plot.
"""
self.fig.canvas.draw()
class HistogramPlot(ACISPlot):
def __init__(self, ds, field, bins=None, range=None, tstart=None,
tstop=None, cumulative=False, density=None,
figsize=(12, 12), fontsize=18, plot=None, **kwargs):
self.field = ds._determine_field(field)
self.xlabel = ds.fields[self.field].display_name
self.unit = ds.fields[self.field].units
slc = slice(tstart, tstop)
self.xx = ds[field][slc]
if plot is None:
fig = plt.figure(figsize=figsize)
ax = fig.add_subplot(111)
else:
fig = plot.fig
ax = plot.ax
super(HistogramPlot, self).__init__(fig, ax, [], None, [])
if self.field[0] == "states":
self.weights = (ds.states["tstop"][slc] -
ds.states["tstart"][slc]).to("ks")
else:
weights = np.diff(self.xx.times.to_value("ks"))
self.weights = Quantity(np.concatenate([weights[0],
0.5*(weights[:-1] +
weights[1:]),
weights[-1]]), "ks")
hist, bins, patches = self.ax.hist(self.xx.value, bins=bins,
range=range, density=density,
cumulative=cumulative,
weights=self.weights.value, **kwargs)
self.hist = hist
self.bins = bins
self.patches = patches
self._annotate_plot(fontsize, density, cumulative)
def _annotate_plot(self, fontsize, density, cumulative):
fontProperties = font_manager.FontProperties(size=fontsize)
for label in self.ax.get_xticklabels():
label.set_fontproperties(fontProperties)
for label in self.ax.get_yticklabels():
label.set_fontproperties(fontProperties)
ulabel = unit_labels.get(self.unit, self.unit)
if self.unit != '':
self.xlabel += f" ({ulabel})"
self.ax.set_xlabel(self.xlabel, fontsize=18)
if density:
self.ylabel = "Fraction of Time"
else:
if cumulative:
self.ylabel = "Cumulative Time (ks)"
else:
self.ylabel = "Time (ks)"
self.ax.set_ylabel(self.ylabel, fontsize=18)
class PhasePlot(ACISPlot):
def __init__(self, ds, x_field, y_field, figsize=(12, 12), plot=None):
if plot is None:
fig = plt.figure(figsize=figsize)
ax = fig.add_subplot(111)
else:
fig = plot.fig
ax = plot.ax
self.x_field = ds._determine_field(x_field)
self.y_field = ds._determine_field(y_field)
self.xlabel = ds.fields[self.x_field].display_name
self.ylabel = ds.fields[self.y_field].display_name
self.xunit = ds.fields[self.x_field].units
self.yunit = ds.fields[self.y_field].units
self.xx = ds[x_field]
self.yy = ds[y_field]
self.ds = ds
super(PhasePlot, self).__init__(fig, ax, [], None, [])
def _annotate_plot(self, fontsize):
fontProperties = font_manager.FontProperties(size=fontsize)
for label in self.ax.get_xticklabels():
label.set_fontproperties(fontProperties)
for label in self.ax.get_yticklabels():
label.set_fontproperties(fontProperties)
uxlabel = unit_labels.get(self.xunit, self.xunit)
uylabel = unit_labels.get(self.yunit, self.yunit)
if self.xunit != '':
self.xlabel += f" ({uxlabel})"
if self.yunit != '':
self.ylabel += f" ({uylabel})"
self.set_xlabel(self.xlabel)
self.set_ylabel(self.ylabel)
return fontProperties
def set_xlim(self, xmin, xmax):
"""
Set the limits on the x-axis of the plot to *xmin* and *xmax*.
"""
self.ax.set_xlim(xmin, xmax)
def set_xlabel(self, xlabel, fontsize=18, **kwargs):
"""
Set the label of the x-axis of the plot.
Parameters
----------
xlabel : string
The new label.
fontsize : integer, optional
The size of the font. Default: 18 pt
Examples
--------
>>> pp.set_xlabel("DEA Temperature", fontsize=15)
"""
fontdict = {"size": fontsize}
self.ax.set_xlabel(xlabel, fontdict=fontdict, **kwargs)
def add_line(self, x, y, lw=2, ls='-', color=None, **kwargs):
"""
Add an arbitrary line to the plot.
Parameters
----------
x : float
The x-values of the line.
y : float
The y-values of the line.
lw : integer, optional
The width of the line. Default: 2
ls : string, optional
The style of the line. Can be one of:
'solid', 'dashed', 'dashdot', 'dotted'.
Default: 'solid'
color : string, optional
The color of the line. Default is to use
the default Matplotlib color.
Examples
--------
>>> x = np.linspace(0.0, 50.0, 100)
>>> y = x.copy()
>>> p.add_line(x, y, lw=3, ls='dashed', color='red')
"""
self.ax.plot(x, y, lw=lw, ls=ls, color=color, **kwargs)
def add_vline(self, x, lw=2, ls='-', color='green', **kwargs):
"""
Add a vertical line on the x-axis of the plot.
Parameters
----------
x : float
The value to place the vertical line at.
lw : integer, optional
The width of the line. Default: 2
ls : string, optional
The style of the line. Can be one of:
'solid', 'dashed', 'dashdot', 'dotted'.
Default: 'solid'
color : string, optional
The color of the line. Default: 'green'
Examples
--------
>>> p.add_vline(25., lw=3, ls='dashed', color='red')
"""
self.ax.axvline(x=x, lw=lw, ls=ls, color=color, **kwargs,
label='_nolegend_')
def add_text(self, x, y, text, fontsize=18, color='black',
rotation='horizontal', **kwargs):
"""
Add text to a PhasePlot.
Parameters
----------
x : string
The x-value to place the text at.
y : float
The y-value to place the text at.
text : string
The text itself.
fontsize : integer, optional
The size of the font. Default: 18 pt.
color : string, optional
The color of the font. Default: black.
rotation : string or float, optional
The rotation of the text. Default: Horizontal
Examples
--------
>>> dp.add_text(32.7, 35., "This spot is interesting",
... fontsize=15, color='magenta')
"""
self.ax.text(x, y, text, fontsize=fontsize, color=color,
rotation=rotation, **kwargs)
[docs]class PhaseScatterPlot(PhasePlot):
r""" Make a single-panel phase scatter plot of one quantity vs. another.
The one restriction is that the two fields must have an equal amount
of samples, achievable by interoplating one field to another's times
or creating a fake MSID field from a state field using
:meth:`~acispy.dataset.Dataset.map_state_to_msid`.
Parameters
----------
ds : :class:`~acispy.dataset.Dataset`
The Dataset instance to get the data to plot from.
x_field : tuple of strings
The field to plot on the x-axis.
y_field : tuple of strings
The field to plot on the y-axis.
c_field : tuple of strings, optional
The field to use to color the dots on the plot. Default: None
fontsize : integer, optional
The font size for the labels in the plot. Default: 18 pt.
color : string, optional
The color of the dots on the phase plot. Only used if a
color field is not provided. Default: 'blue'
cmap : string, optional
The colormap for the dots if a color field has been provided.
Default: 'heat'
figsize : tuple of integers, optional
The size of the plot in (width, height) in inches. Default: (12, 12)
plot : :class:`~acispy.plots.PhasePlot`, optional
An existing PhasePlot to add this plot to. Default: None, one
will be created if not provided.
Examples
--------
>>> from acispy import PhaseScatterPlot
>>> pp = PhaseScatterPlot(ds, ("msids", "1deamzt"), ("msids", "1dpamzt"))
"""
def __init__(self, ds, x_field, y_field, c_field=None,
fontsize=18, color='blue', cmap='hot',
figsize=(12, 12), plot=None, **kwargs):
super(PhaseScatterPlot, self).__init__(ds, x_field, y_field,
figsize=figsize, plot=plot)
if c_field is None:
self.cc = color
else:
self.cc = np.array(ds[c_field])
cm = plt.cm.get_cmap(cmap)
pp = self.ax.scatter(np.array(self.xx), np.array(self.yy),
c=self.cc, cmap=cm, **kwargs)
self.pp = pp
fontProperties = self._annotate_plot(fontsize)
if c_field is not None:
clabel = self.ds.fields[c_field].display_name
cunit = self.ds.fields[c_field].units
if cunit != '':
uclabel = unit_labels.get(cunit, cunit)
clabel += f" ({uclabel})"
divider = make_axes_locatable(self.ax)
cax = divider.append_axes("right", size="5%", pad=0.05)
cb = plt.colorbar(self.pp, cax=cax)
fontdict = {"size": fontsize}
cb.set_label(clabel, fontdict=fontdict)
for label in cb.ax.get_yticklabels():
label.set_fontproperties(fontProperties)
self.cb = cb
[docs]class PhaseHistogramPlot(PhasePlot):
r""" Make a single-panel 2D binned histogram plot of one quantity
vs. another.
The one restriction is that the two fields must have an equal amount
of samples, achievable by interoplating one field to another's times
or creating a fake MSID field from a state field using
:meth:`~acispy.dataset.Dataset.map_state_to_msid`.
Parameters
----------
ds : :class:`~acispy.dataset.Dataset`
The Dataset instance to get the data to plot from.
x_field : tuple of strings
The field to plot on the x-axis.
y_field : tuple of strings
The field to plot on the y-axis.
x_bins : int or NumPy array
The bins for the x-axis of the histogram. If an int, it will
make that many bins between the minimum and maximum values.
If a NumPy array, it will use it as the bin edges.
y_bins : int or NumPy array
The bins for the y-axis of the histogram. If an int, it will
make that many bins between the minimum and maximum values.
If a NumPy array, it will use it as the bin edges.
scale : string, optional
The scaling of the plot. "linear" or "log". Default: "linear"
cmap : string, optional
The colormap for the histogram. Default: 'heat'
fontsize : integer, optional
The font size for the labels in the plot. Default: 18 pt.
figsize : tuple of integers, optional
The size of the plot in (width, height) in inches. Default: (12, 12)
plot : :class:`~acispy.plots.PhasePlot`, optional
An existing PhasePlot to add this plot to. Default: None, one
will be created if not provided.
Examples
--------
>>> from acispy import PhaseHistogramPlot
>>> pp = PhaseHistogramPlot(ds, "1deamzt", "1dpamzt", 100, 100)
"""
def __init__(self, ds, x_field, y_field, x_bins, y_bins, scale='linear',
cmap='hot', fontsize=18, figsize=(12, 12), plot=None, **kwargs):
from matplotlib.colors import LogNorm, Normalize
super(PhaseHistogramPlot, self).__init__(ds, x_field, y_field,
figsize=figsize, plot=plot)
cm = plt.cm.get_cmap(cmap)
if scale == "log":
norm = LogNorm()
else:
norm = Normalize()
counts, xedges, yedges, pp = self.ax.hist2d(np.asarray(self.xx), np.asarray(self.yy),
[x_bins, y_bins], cmap=cm, norm=norm, **kwargs)
self.pp = pp
self.counts = counts
self.xedges = xedges
self.yedges = yedges
fontProperties = self._annotate_plot(fontsize)
divider = make_axes_locatable(self.ax)
cax = divider.append_axes("right", size="5%", pad=0.05)
cb = plt.colorbar(self.pp, cax=cax)
fontdict = {"size": fontsize}
cb.set_label("Counts", fontdict=fontdict)
for label in cb.ax.get_yticklabels():
label.set_fontproperties(fontProperties)
self.cb = cb