#
# This file is part of moniplot
#
# https://github.com/rmvanhees/moniplot.git
#
# Copyright (c) 2022-2023 SRON - Netherlands Institute for Space Research
#
# License: GPLv3
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
This module contains the class `MONplot` with the methods:
`draw_hist`, `draw_lplot`, `draw_multiplot`, `draw_qhist`, `draw_quality`,
`draw_signal`, `draw_tracks`, `draw_trend`, draw_fov_ckd.
"""
from __future__ import annotations
__all__ = ['MONplot']
from datetime import datetime
from pathlib import Path
import numpy as np
import xarray as xr
try:
from cartopy import crs as ccrs
except ModuleNotFoundError:
FOUND_CARTOPY = False
else:
FOUND_CARTOPY = True
import matplotlib as mpl
import matplotlib.pyplot as plt
from matplotlib.backends.backend_pdf import PdfPages
from matplotlib.gridspec import GridSpec
from .biweight import Biweight
from .lib.fig_draw_image import (adjust_img_ticks, fig_data_to_xarr,
fig_draw_panels, fig_qdata_to_xarr)
from .lib.fig_draw_lplot import close_draw_lplot, fig_draw_lplot
from .lib.fig_draw_multiplot import draw_subplot, get_xylabels
from .lib.fig_draw_qhist import fig_draw_qhist
from .lib.fig_draw_trend import add_hk_subplot, add_subplot
from .lib.fig_info import FIGinfo
from .tol_colors import tol_rgba
if FOUND_CARTOPY:
from .lib.fig_draw_tracks import fig_draw_tracks
# - global variables -------------------------------
DEFAULT_CSET = 'bright'
# - local functions --------------------------------
# - main function ----------------------------------
[docs]class MONplot:
"""
Generate PDF reports (or figures) to facilitate instrument calibration
or monitoring.
Parameters
----------
figname : Path | str
Name of PDF or PNG file (extension required)
caption : str, optional
Caption repeated on each page of the PDF
Notes
-----
The methods of the class `MONplot` will accept `numpy` arrays as input and
display your data without knowledge on the data units and coordinates.
In most cases, this will be enough for a quick inspection of your data.
However, when you use the labeled arrays and datasets of `xarray`then
the software will use the name of the xarray class, coordinate names and
data attributes, such as `long_name` and `units`.
"""
def __init__(self, figname: Path | str, caption: str | None = None):
"""Initialize multi-page PDF document or a single-page PNG.
"""
self.__cset = tol_rgba(DEFAULT_CSET)
self.__cmap = None
self.__caption = '' if caption is None else caption
self.__institute = ''
self.__mpl: dict | None = None
self.__pdf = None
self.filename = Path(figname)
if self.filename.suffix.lower() != '.pdf':
return
self.__pdf = PdfPages(self.filename)
# turn-off the automatic offset notation of Matplotlib
mpl.rcParams['axes.formatter.useoffset'] = False
def __repr__(self) -> None:
pass
def __close_this_page(self, fig) -> None:
"""Save the current figure and close the MONplot instance.
"""
# add save figure
if self.__pdf is None:
plt.savefig(self.filename)
plt.close(fig)
else:
self.__pdf.savefig()
[docs] def close(self) -> None:
"""Close PNG or (multipage) PDF document."""
if self.__pdf is None:
return
# add PDF annotations
doc = self.__pdf.infodict()
if self.__caption is not None:
doc['Title'] = self.__caption
doc['Subject'] = \
'Generated using https://github.com/rmvanhees/moniplot.git'
if self.__institute == 'SRON':
doc['Author'] = '(c) SRON Netherlands Institute for Space Research'
elif self.__institute:
doc['Author'] = f'(c) {self.__institute}'
self.__pdf.close()
plt.close('all')
@property
def caption(self) -> str:
"""Returns caption of figure."""
return self.__caption
[docs] def set_caption(self, caption: str) -> None:
"""Set caption of each page of the PDF.
Parameters
----------
caption : str
Default title of all pages at the top of the page.
"""
self.__caption = caption
def __add_caption(self, fig):
"""Add figure caption.
"""
if not self.caption:
return
fig.suptitle(self.caption, fontsize='x-large',
position=(0.5, 1 - 0.3 / fig.get_figheight()))
# --------------------------------------------------
@property
def cmap(self):
"""Returns current Matplotlib colormap."""
return self.__cmap
[docs] def set_cmap(self, cmap) -> None:
"""Use alternative colormap for MONplot::draw_image.
Parameters
----------
cmap : matplotlib colormap
"""
self.__cmap = cmap
[docs] def unset_cmap(self) -> None:
"""Unset user supplied colormap, and use default colormap.
"""
self.__cmap = None
# --------------------------------------------------
@property
def cset(self) -> str:
"""Returns name of current color-set."""
return self.__cset
[docs] def set_cset(self, cname: str, cnum: int | None = None) -> None:
"""Use alternative color-set through which `draw_lplot` will cycle.
Parameters
----------
cname : str
Name of color set. Use None to get the default matplotlib value.
cnum : int, optional
Number of discrete colors in colormap (*not colorset*).
"""
self.__cset = tol_rgba(cname, cnum)
[docs] def unset_cset(self) -> None:
"""Set color set to its default.
"""
self.__cset = tol_rgba(DEFAULT_CSET)
# --------------------------------------------------
@property
def institute(self) -> str:
"""Returns name of institute."""
return self.__institute
[docs] def set_institute(self, institute: str) -> None:
"""Use the name of your institute as a signature.
Parameters
----------
institute : str
Provide abbreviation of the name of your institute to be used in
the copyright statement in the main panel of the figures.
"""
self.__institute = institute
# --------------------------------------------------
def __add_copyright(self, axx) -> None:
"""Show value of institute as copyright in the lower right corner
of the current figure.
"""
if not self.institute:
return
axx.text(1, 0, rf' $\copyright$ {self.institute}',
horizontalalignment='right',
verticalalignment='bottom',
rotation='vertical', fontsize='xx-small',
transform=axx.transAxes)
@staticmethod
def __add_fig_box(fig, fig_info: FIGinfo) -> None:
"""Add a box with meta information in the current figure.
Parameters
----------
fig : Matplotlib figure instance
fig_info : FIGinfo
instance of pys5p.lib.plotlib.FIGinfo to be displayed
"""
if fig_info is None or fig_info.location != 'above':
return
xpos = 1 - 0.4 / fig.get_figwidth()
ypos = 1 - 0.25 / fig.get_figheight()
fig.text(xpos, ypos, fig_info.as_str(),
fontsize='x-small', style='normal',
verticalalignment='top',
horizontalalignment='right',
multialignment='left',
bbox={'facecolor': 'white', 'pad': 5})
# -------------------------
def __draw_image__(self, xarr: xr.DataArray, side_panels: str,
fig_info: FIGinfo | None, title: str | None) -> None:
"""Does the actual drawing of the image data for the public methods
`draw_signal` and `draw_quality`.
"""
def add_fig_box() -> None:
"""Add a box with meta information in the current figure.
"""
if fig_info is None:
return
if fig_info.location == 'above':
if aspect <= 2:
halign = 'left' if aspect == 1 else 'center'
fontsize = 'x-small'
else:
halign = 'right'
fontsize = 'xx-small' if len(fig_info) > 6 else 'x-small'
axx_c.text(0 if aspect <= 2 else 1,
1.04 + (aspect-1) * 0.0075,
fig_info.as_str(), fontsize=fontsize,
transform=axx_c.transAxes,
multialignment='left',
verticalalignment='bottom',
horizontalalignment=halign,
bbox={'facecolor': 'white', 'pad': 4})
return
if fig_info.location == 'below':
fontsize = 'xx-small' if aspect in (3, 4) else 'x-small'
axx_c.text(0.125 + (aspect-1) * 0.2,
-0.03 - (aspect-1) * 0.005,
fig_info.as_str(), fontsize=fontsize,
transform=axx_c.transAxes,
multialignment='left',
verticalalignment='top',
horizontalalignment='left',
bbox={'facecolor': 'white', 'pad': 4})
# aspect of image data
aspect = min(4, max(1, int(round(xarr.shape[1] / xarr.shape[0]))))
# select figure attributes
attrs = {1: {'figsize': (10, 8),
'w_ratios': (1., 7., 0.5, 1.5),
'h_ratios': (7., 1.)}, # 7 x 7
2: {'figsize': (13, 6.25),
'w_ratios': (1., 10., 0.5, 1.5),
'h_ratios': (5., 1.)}, # 10 x 5
3: {'figsize': (15, 5.375),
'w_ratios': (1., 12., 0.5, 1.5),
'h_ratios': (4., 1.)}, # 12 x 4
4: {'figsize': (17, 5.125),
'w_ratios': (1., 14., 0.5, 1.5),
'h_ratios': (3.5, 1.)}}.get(aspect) # 14 x 3.5
# define matplotlib figure
fig = plt.figure(figsize=attrs['figsize'])
if self.caption:
fig.suptitle(self.caption, fontsize='x-large',
position=(0.5, 1 - 0.4 / fig.get_figheight()))
# Define a grid layout to place subplots within the figure.
# - gspec[0, 1] is reserved for the image
# - gspec[1, 1] is reserved for the x-panel
# - gspec[0, 0] is reserved for the y-panel
# - gspec[0, 2] is reserved for the colorbar
# - gspec[1, 2] is used to pace the small fig_info box (max 6/7 lines)
gspec = fig.add_gridspec(2, 4,
left=.135 + .005 * (aspect-1),
right=.9 - .005 * (aspect-1),
top=.865 - .025 * (aspect-1),
bottom=.115 + .01 * (aspect-1),
wspace=0.1 / max(2, aspect-1),
hspace=0.05,
width_ratios=attrs['w_ratios'],
height_ratios=attrs['h_ratios'])
# add image panel and draw image
axx = fig.add_subplot(gspec[0, 1])
if xarr.attrs['_zscale'] == 'quality':
img = axx.imshow(xarr.values, norm=xarr.attrs['_znorm'],
aspect='auto', cmap=xarr.attrs['_cmap'],
interpolation='none', origin='lower')
else:
cmap = self.cmap if self.cmap else xarr.attrs['_cmap']
img = axx.imshow(xarr.values, norm=xarr.attrs['_znorm'],
aspect='auto', cmap=cmap,
interpolation='none', origin='lower')
# add title to image panel
if title is not None:
axx.set_title(title)
elif 'long_name' in xarr.attrs:
axx.set_title(xarr.attrs['long_name'])
self.__add_copyright(axx)
# axx.grid(True)
# add side panels
if side_panels == 'none':
adjust_img_ticks(axx, xarr)
axx.set_xlabel(xarr.dims[1])
axx.set_ylabel(xarr.dims[0])
else:
for xtl in axx.get_xticklabels():
xtl.set_visible(False)
for ytl in axx.get_yticklabels():
ytl.set_visible(False)
axx_p = {'X': fig.add_subplot(gspec[1, 1], sharex=axx),
'Y': fig.add_subplot(gspec[0, 0], sharey=axx)}
fig_draw_panels(axx_p, xarr, side_panels)
axx_p['X'].set_xlabel(xarr.dims[1])
axx_p['Y'].set_ylabel(xarr.dims[0])
# add colorbar
if xarr.attrs['_zscale'] == 'quality':
axx_c = fig.add_subplot(gspec[0, 2])
bounds = xarr.attrs['flag_values']
mbounds = [(bounds[ii+1] + bounds[ii]) / 2
for ii in range(len(bounds)-1)]
_ = plt.colorbar(img, cax=axx_c, ticks=mbounds, boundaries=bounds)
axx_c.tick_params(axis='y', which='both', length=0)
axx_c.set_yticklabels(xarr.attrs['flag_meanings'])
else:
axx_c = fig.add_subplot(gspec[0, 2])
_ = plt.colorbar(img, cax=axx_c, label=xarr.attrs['_zlabel'])
# add annotation and save the figure
add_fig_box()
self.__close_this_page(fig)
# --------------------------------------------------
[docs] def draw_signal(self, data: xr.DataArray | np.ndarray, *,
fig_info: FIGinfo | None = None,
side_panels: str = 'nanmedian',
title: str | None = None, **kwargs) -> None:
"""Display 2D array as an image and averaged column/row signal
in the side-panels (optional).
Parameters
----------
data : numpy.ndarray or xarray.DataArray
Object holding measurement data and attributes.
fig_info : FIGinfo, default=None
OrderedDict holding meta-data to be displayed in the figure.
side_panels : str, default='nanmedian'
Show image row and column statistics in two side panels.
Use 'none' when you do not want the side panels.
Other valid values are: 'median', 'nanmedian', 'mean', 'nanmean',
'quality', 'std' and 'nanstd'.
title : str, default=None
Title of this figure using `Axis.set_title`.
**kwargs : other keywords
Pass keyword arguments: `zscale`, `vperc` or `vrange`
to `moniplot.lib.fig_draw_image.fig_data_to_xarr()`.
See also
--------
fig_data_to_xarr : Prepare image data for plotting.
Notes
-----
When data is a xarray.DataArray then the following attributes
are used::
'long_name' : used as the title of the main panel when parameter \
`title` is not defined.
'_cmap' : contains the matplotlib colormap
'_zlabel' : contains the label of the color bar
'_znorm' : matplotlib class to normalize the data between zero \
and one.
'_zscale' : scaling of the data values: linear, log, diff, ratio, ...
'_zunits' : adjusted units of the data
The information provided in the parameter `fig_info` will be displayed
in a text box. In addition, we display the creation date and the data
(biweight) median & spread.
Currently, we have turned off the automatic offset notation of
`matplotlib`. Maybe this should be the default, which the user may
override.
Examples
--------
Create a PDF document 'test.pdf' and add figure of dataset img
(`numpy.ndarray` or `xarray.DataArray`) with side-panels and title::
> plot = MONplot('test.pdf', caption='my caption')
> plot.set_institute('SRON')
> plot.draw_signal(img, title='my title')
Add the same figure without side-panels::
> plot.draw_signal(img, side_panels='none', title='my title')
Add a figure using a fixed data-range that the colormap covers::
> plot.draw_signal(img1, title='my title', vrange=[zmin, zmax])
Add a figure where img2 = img - img_ref::
> plot.draw_signal(img2, title='my title', zscale='diff')
Add a figure where img2 = img / img_ref::
> plot.draw_signal(img2, title='my title', zscale='ratio')
Finalize the PDF file::
> plot.close()
"""
# convert, if necessary, input data to xarray.DataArray
if isinstance(data, xr.DataArray) and '_zscale' in data.attrs:
xarr = data.copy()
else:
xarr = fig_data_to_xarr(data, **kwargs)
# add data statistics to fig_info
if fig_info is None:
fig_info = FIGinfo()
biwght = Biweight(xarr.values)
if xarr.attrs['_zunits'] is None or xarr.attrs['_zunits'] == '1':
fig_info.add('median', biwght.median, '{:.5g}')
fig_info.add('spread', biwght.spread, '{:.5g}')
else:
fig_info.add('median', (biwght.median, xarr.attrs['_zunits']),
'{:.5g} {}')
fig_info.add('spread', (biwght.spread, xarr.attrs['_zunits']),
'{:.5g} {}')
# draw actual image
self.__draw_image__(xarr, side_panels, fig_info, title)
# --------------------------------------------------
[docs] def draw_quality(self, data: xr.DataArray | np.ndarray,
ref_data: np.ndarray | None = None, *,
side_panels: str = 'quality',
fig_info: FIGinfo | None = None,
title: str | None = None, **kwargs) -> None:
"""Display pixel-quality 2D array as image with column/row statistics.
Parameters
----------
data : numpy.ndarray or xarray.DataArray
Object holding measurement data and attributes.
ref_data : numpy.ndarray, default=None
Numpy array holding reference data, for example pixel quality
reference map taken from the CKD. Shown are the changes with
respect to the reference data.
fig_info : FIGinfo, default=None
OrderedDict holding meta-data to be displayed in the figure.
side_panels : str, default='quality'
Show image row and column statistics in two side panels.
Use 'none' when you do not want the side panels.
title : str, default=None
Title of this figure using `Axis.set_title`.
**kwargs : other keywords
Pass keyword arguments: `data_sel`, `thres_worst`, `thres_bad`
or `qlabels` to `moniplot.lib.fig_draw_image.fig_qdata_to_xarr`.
See Also
--------
qdata_to_xarr : Prepare pixel-quality data for plotting.
Notes
-----
When data is a xarray.DataArray then the following attributes
are used::
'long_name' : used as the title of the main panel when parameter \
`title` is not defined.
'flag_values' : values of the flags used to qualify the pixel quality
'flag_meanings' : description of the flag values
'thres_bad' : threshold between good and bad
'thres_worst' : threshold between bad and worst
'_cmap' : contains the matplotlib colormap
'_zscale' : should be 'quality'
'_znorm' : matplotlib class to normalize the data between zero \
and one
The quality ranking labels are ['unusable', 'worst', 'bad', 'good'],
when no reference dataset is provided. Where::
'unusable' : pixels outside the illuminated region
'worst' : 0 <= value < thres_worst
'bad' : 0 <= value < thres_bad
'good' : thres_bad <= value <= 1
Otherwise, the labels for quality ranking indicate which pixels have
changed w.r.t. reference. The labels are::
'unusable' : pixels outside the illuminated region
'worst' : from good or bad to worst
'bad' : from good to bad
'good' : from any rank to good
'unchanged' : no change in rank
The information provided in the parameter `fig_info` will be displayed
in a small box. Where creation date and statistics on the number of
bad and worst pixels are displayed.
Examples
--------
Create a PDF document 'test.pdf' and add figure of dataset img
(`numpy.ndarray` or `xarray.DataArray`) with side-panels and title::
> plot = MONplot('test.pdf', caption='my caption', institute='SRON')
> plot.draw_quality(img, title='my title')
Add the same figure without side-panels::
> plot.draw_quality(img, side_panels='none', title='my title')
Add a figure where img_ref is a quality map from early in the mission::
> plot.draw_quality(img, img_ref, title='my title')
Finalize the PDF file::
> plot.close()
"""
# convert, if necessary, input data to xarray.DataArray
if isinstance(data, xr.DataArray) and '_zscale' in data.attrs:
xarr = data
else:
xarr = fig_qdata_to_xarr(data, ref_data, **kwargs)
# add statistics on data quality to fig_info
if fig_info is None:
fig_info = FIGinfo()
if ref_data is None:
fig_info.add(
f'{xarr.attrs["flag_meanings"][2]}'
f' (quality < {xarr.attrs["thres_bad"]})',
np.sum((xarr.values == 1) | (xarr.values == 2)))
fig_info.add(
f'{xarr.attrs["flag_meanings"][1]}'
f' (quality < {xarr.attrs["thres_worst"]})',
np.sum(xarr.values == 1))
else:
fig_info.add(xarr.attrs['flag_meanings'][3],
np.sum(xarr.values == 4))
fig_info.add(xarr.attrs['flag_meanings'][2],
np.sum(xarr.values == 2))
fig_info.add(xarr.attrs['flag_meanings'][1],
np.sum(xarr.values == 1))
# draw actual image
self.__draw_image__(xarr, side_panels, fig_info, title)
# --------------------------------------------------
[docs] def draw_trend(self, xds: xr.DataArray | None = None,
hk_xds: xr.DataArray | None = None, *,
fig_info: FIGinfo | None = None,
title: str | None = None, **kwargs) -> None:
"""
Display trends of measurement data and/or housekeeping data
Parameters
----------
xds : xarray.Dataset, optional
Object holding measurement data and attributes.
hk_xds : xarray.Dataset, optional
Object holding housekeeping data and attributes.
fig_info : FIGinfo, optional
OrderedDict holding meta-data to be displayed in the figure.
title : str, optional
Title of this figure using `Axis.set_title`.
**kwargs : other keywords
Pass keyword arguments: 'vperc' or 'vrange_last_orbits'
to 'moniplot.lib.fig_draw_trend.add_hk_subplot`.
See Also
--------
add_hk_subplot : Add a subplot for housekeeping data.
Notes
-----
When data is a xarray.DataArray then the following attributes are used:
- long_name: used as the title of the main panel when parameter 'title'\
is not defined.
- units: units of the data
Examples
--------
Create a PDF document 'test.pdf' and add figure of dataset 'xds'
(`numpy.ndarray` or `xarray.DataArray`) with a title. The dataset 'xds'
may contain multiple DataArrays with a common X-coordinate. Each
DataArray will be displayed in a separate sub-panel::
> plot = MONplot('test.pdf', caption='my caption', institute='SRON')
> plot.draw_trend(xds, hk_xds=None, title='my title')
Add a figure with the same Dataset 'xds' and a few trends of
housekeeping data (again each parameter in a separate DataArray with
a common X-coordinate)::
> plot.draw_trend(xds, hk_xds, title='my title')
Finalize the PDF file::
> plot.close()
"""
if xds is None and hk_xds is None:
raise ValueError('both xds and hk_xds are None')
if xds is not None and not isinstance(xds, xr.Dataset):
raise ValueError('xds should be and xarray Dataset object')
if hk_xds is not None and not isinstance(hk_xds, xr.Dataset):
raise ValueError('hk_xds should be and xarray Dataset object')
if fig_info is None:
fig_info = FIGinfo()
# determine npanels from xarray Dataset
npanels = len(xds.data_vars) if xds is not None else 0
npanels += len(hk_xds.data_vars) if hk_xds is not None else 0
# initialize matplotlib using 'subplots'
figsize = (10., 1 + (npanels + 1) * 1.5)
fig, axarr = plt.subplots(npanels, sharex='all', figsize=figsize)
if npanels == 1:
axarr = [axarr]
margin = min(1. / (1.65 * (npanels + 1)), .25)
fig.subplots_adjust(bottom=margin, top=1-margin, hspace=0.05)
# add a centered subtitle to the figure
self.__add_caption(fig)
# add title to image panel
if title is not None:
axarr[0].set_title(title)
# add figures with trend data
ipanel = 0
xlabel = 'time'
if xds is not None:
xlabel = 'orbit' if 'orbit' in xds.coords else 'time [hours]'
for name in xds.data_vars:
add_subplot(axarr[ipanel], xds[name])
ipanel += 1
if hk_xds is not None:
xlabel = 'orbit' if 'orbit' in hk_xds.coords else 'time [hours]'
for name in hk_xds.data_vars:
add_hk_subplot(axarr[ipanel], hk_xds[name], **kwargs)
ipanel += 1
# finally add a label for the X-coordinate
axarr[-1].set_xlabel(xlabel)
# add annotation and save the figure
self.__add_copyright(axarr[-1])
self.__add_fig_box(fig, fig_info)
self.__close_this_page(fig)
# --------------------------------------------------
[docs] def draw_hist(self, data: xr.DataArray | np.ndarray,
data_sel: tuple[slice | int] | None = None,
vrange: list[float, float] | None = None,
fig_info: FIGinfo | None = None,
title: str | None = None, **kwargs) -> None:
r"""Display data as histograms.
Parameters
----------
data : numpy.ndarray or xarray.DataArray
Object holding measurement data and attributes.
data_sel : mask or index tuples for arrays, optional
Select a region on the detector by fancy indexing (using a
boolean/integer arrays), or using index tuples for arrays
(generated with `numpy.s\_`).
vrange : list[float, float], default=[data.min(), data.max()]
The lower and upper range of the bins.
Note data will also be clipped according to this range.
fig_info : FIGinfo, optional
OrderedDict holding meta-data to be displayed in the figure.
title : str, optional
Title of this figure using `Axis.set_title`.
Default title is 'f'Histogram of {data.attrs["long_name"]}'.
**kwargs : other keywords
Pass the following keyword arguments to `matplotlib.pyplot.hist`:
'bins', 'density' or 'log'.
Note that keywords: 'histtype', 'color', 'linewidth' and 'fill'
are predefined.
See Also
--------
matplotlib.pyplot.hist : Compute and plot a histogram.
Notes
-----
When data is a xarray.DataArray then the following attributes are used:
- long_name: used as the title of the main panel when parameter 'title'\
is not defined.
- units: units of the data
Examples
--------
Create a PDF document 'test.pdf' with two pages.
Both pages have the caption "My Caption", the title of the figure on
the first page is "my title" and on the second page the title of the
figure is `f"Histogram of {xarr.attrs['long_name']}"`, if xarr has
attribute "long_name"::
> plot = MONplot('test.pdf', caption='My Caption')
> plot.set_institute('SRON')
> plot.draw_hist(data, title='my title')
> plot.draw_hist(xarr)
> plot.close()
"""
long_name = ''
zunits = '1'
if isinstance(data, xr.DataArray):
if data_sel is None:
values = data.values.reshape(-1)
else:
values = data.values[data_sel].reshape(-1)
if 'units' in data.attrs:
zunits = data.attrs['units']
if 'long_name' in data.attrs:
long_name = data.attrs['long_name']
else:
if data_sel is None:
values = data.reshape(-1)
else:
values = data[data_sel].reshape(-1)
# add data statistics to fig_info
if fig_info is None:
fig_info = FIGinfo()
biwght = Biweight(values)
if zunits == '1':
fig_info.add('median', biwght.median, '{:.5g}')
fig_info.add('spread', biwght.spread, '{:.5g}')
else:
fig_info.add('median', (biwght.median, zunits), '{:.5g} {}')
fig_info.add('spread', (biwght.spread, zunits), '{:.5g} {}')
# create figure
fig, axx = plt.subplots(1, figsize=(9, 8))
# add a centered subtitle to the figure
self.__add_caption(fig)
# add title to image panel and set xlabel
xlabel = 'value' if zunits == '1' else f'value [{zunits}]'
if title is None:
title = f'Histogram of {long_name}'
elif long_name:
xlabel = long_name if zunits == '1' else f'{long_name} [{zunits}]'
axx.set_title(title)
# add histogram
if vrange is not None:
values = np.clip(values, vrange[0], vrange[1])
# Edgecolor is tol_cset('bright').blue
if 'bins' in kwargs and kwargs['bins'] > 24:
axx.hist(values, range=vrange, histtype='step',
edgecolor='#4477AA', facecolor='#77AADD',
fill=True, linewidth=1.5, **kwargs)
axx.grid(which='major', color='#AAAAAA', linestyle='--')
else:
axx.hist(values, range=vrange, histtype='bar',
edgecolor='#4477AA', facecolor='#77AADD',
linewidth=1.5, **kwargs)
axx.grid(which='major', axis='y', color='#AAAAAA', linestyle='--')
axx.set_xlabel(xlabel)
if 'density' in kwargs and kwargs['density']:
axx.set_ylabel('density')
else:
axx.set_ylabel('number')
if len(fig_info) > 3:
plt.subplots_adjust(top=.875)
# add annotation and save the figure
self.__add_copyright(axx)
self.__add_fig_box(fig, fig_info)
self.__close_this_page(fig)
# --------------------------------------------------
[docs] def draw_qhist(self, xds: xr.DataArray,
data_sel: tuple[slice, int] | None = None,
density: bool = True,
fig_info: FIGinfo | None = None,
title: str | None = None) -> None:
r"""Display pixel-quality data as histograms.
Parameters
----------
xds : xarray.Dataset
Object holding measurement data and attributes.
data_sel : mask or index tuples for arrays, optional
Select a region on the detector by fancy indexing (using a
boolean/interger arrays), or using index tuples for arrays
(generated with `numpy.s\_`).
density : bool, default=True
If True, draw and return a probability density: each bin will
display the bin's raw count divided by the total number of counts
and the bin width (see `matplotlib.pyplot.hist`).
fig_info : FIGinfo, optional
OrderedDict holding meta-data to be displayed in the figure.
title : str, optional
Title of this figure using `Axis.set_title`.
See Also
--------
matplotlib.pyplot.hist : Compute and plot a histogram.
Notes
-----
When data is a xarray.DataArray then the following attributes are used:
- long_name: used as the title of the main panel when parameter 'title'\
is not defined.
- units: units of the data
Examples
--------
Create a PDF document 'test.pdf' and add figure of dataset 'xds'
(`numpy.ndarray` or `xarray.DataArray`) with a title. The dataset 'xds'
may contain multiple DataArrays with a common X-coordinate. Each
DataArray will be displayed in a seperate sub-panel.
>>> plot = MONplot('test.pdf', caption='my caption', institute='SRON')
>>> plot.draw_qhist(xds, title='my title')
>>> plot.close()
"""
if not isinstance(xds, xr.Dataset):
raise ValueError('xds should be and xarray Dataset object')
if fig_info is None:
fig_info = FIGinfo()
# determine npanels from xarray Dataset
npanels = len(xds.data_vars)
# initialize matplotlib using 'subplots'
figsize = (10., 1 + (npanels + 1) * 1.65)
fig, axarr = plt.subplots(npanels, sharex='all', figsize=figsize)
if npanels == 1:
axarr = [axarr]
margin = min(1. / (1.8 * (npanels + 1)), .25)
fig.subplots_adjust(bottom=margin, top=1-margin, hspace=0.02)
# add a centered subtitle to the figure
self.__add_caption(fig)
# add title to image panel
if title is None:
title = 'Histograms of pixel-quality'
axarr[0].set_title(title)
# add figures with histograms
for ii, (key, xda) in enumerate(xds.data_vars.items()):
if data_sel is None:
qdata = xda.values.reshape(-1)
else:
qdata = xda.values[data_sel].reshape(-1)
label = xda.attrs['long_name'] if 'long_name' in xda.attrs else key
fig_draw_qhist(axarr[ii], qdata, label, density)
# finally add a label for the X-coordinate
axarr[-1].set_xlabel('pixel quality')
# add annotation and save the figure
self.__add_copyright(axarr[-1])
self.__add_fig_box(fig, fig_info)
self.__close_this_page(fig)
# --------------------------------------------------
[docs] def draw_lplot(self, xdata: np.ndarray | None = None,
ydata: np.ndarray | None = None, *,
square: bool = False,
fig_info: FIGinfo | None = None,
title: str | None = None,
kwlegend: dict | None = None, **kwargs) -> None:
"""Plot y versus x lines, maybe called multiple times to add lines.
Figure is closed when called with xdata equals None.
Parameters
----------
xdata : ndarray, optional
``[add line]`` X-data.
ydata : ndarray, optional
``[add line]`` Y-data, or
``[close figure]`` when ydata is None.
square : bool, default=False
``[add line]`` create a square figure,
independent of number of data-points (*first call, only*).
fig_info : FIGinfo, optional
``[close figure]`` Meta-data to be displayed in the figure.
title : str, optional
``[close figure]`` Title of figure (using `Axis.set_title`).
kwlegend : dict, optional
``[close figure]`` Provide keywords for the function `Axes.legend`.
Default: {'fontsize': 'small', 'loc': 'best'}
**kwargs : other keywords
``[add line]`` Keywords are passed to `matplotlib.pyplot.plot`;
``[close figure]`` Keywords are passed to appropriate
`matplotlib.Axes` method, and the keyword 'text' can be used to
add addition text in the upper left corner.
Examples
--------
General example::
> plot = MONplot(fig_name)
> for ii, xarr, yarr in enumerate(data_of_each_line):
... plot.draw_lplot(xarr, yarr, label=mylabel[ii], marker='o')
> plot.draw_lplot(xlim=[0, 0.5], ylim=[-10, 10],
... xlabel='x-axis, ylabel=y-axis')
> plot.close()
Using a time-axis::
> from datetime import datetime, timedelta
> tt0 = (datetime(year=2020, month=10, day=1)
... + timedelta(seconds=sec_in_day))
> tt = [tt0 + iy * t_step for iy in range(yy.size)]
> plot = MONplot(fig_name)
> plot.draw_lplot(tt, yy, label='mylabel', marker='o')
> plot.draw_lplot(ylim=[-10, 10], xlabel='t-axis', ylabel='y-axis')
> plot.close()
You can use different sets of colors and cycle through them.
First, we use default colors defined by matplotlib::
> plot = MONplot('test_lplot.pdf')
> plot.set_cset(None)
> for ii in range(5):
... plot.draw_lplot(np.arange(10), np.arange(10)*(ii+1))
> plot.draw_lplot(xlabel='x-axis', ylabel='y-axis',
... title='draw_lplot [cset is None]')
You can also assign colors to each line::
> for ii, clr in enumerate('rgbym'):
... plot.draw_lplot(np.arange(10), np.arange(10)*(ii+1), color=clr)
> plot.draw_lplot(xlabel='x-axis', ylabel='y-axis',
... title='draw_lplot [cset="rgbym"]')
You can use one of the color sets as defined in ``tol_colors``::
> plot.set_cset('mute') # Note the default is 'bright'
> for ii in range(5):
... plot.draw_lplot(ydata=np.arange(10)*(ii+1))
> plot.draw_lplot(xlabel='x-axis', ylabel='y-axis',
... title='draw_lplot [cset="mute"]')
Or you can use a color map as defined in ``tol_colors`` where
you can define the number of colors you need. If you need less than 24
colors, you can use 'rainbow_discrete' or you can choose one
color map if you need more colors, for example::
> plot.set_cset('rainbow_PuBr', 25)
> for ii in range(25):
... plot.draw_lplot(ydata=np.arange(10)*(ii+1))
> plot.draw_lplot(xlabel='x-axis', ylabel='y-axis',
... title='draw_lplot [cset="rainbow_PyBr"]')
> plot.close()
"""
if ydata is None:
if self.__mpl is None:
raise ValueError('No plot defined and no data provided')
fig = self.__mpl['fig']
axx = self.__mpl['axx']
if fig_info is None:
fig_info = FIGinfo()
if 'text' in kwargs:
axx.text(0.05, 0.985, kwargs.pop('text'),
transform=axx.transAxes,
fontsize='small', verticalalignment='top',
bbox={'boxstyle': 'round', 'alpha': 0.5,
'facecolor': '#FFFFFF', 'edgecolor': '#BBBBBB'})
close_draw_lplot(axx, self.__mpl['time_axis'],
title, kwlegend, **kwargs)
# add annotation and save the figure
self.__add_copyright(axx)
self.__add_fig_box(fig, fig_info)
self.__close_this_page(fig)
self.__mpl = None
return
# initialize figure
if xdata is None:
xdata = np.arange(ydata.size)
if self.__mpl is None:
if square:
figsize = (9, 9)
else:
figsize = {0: (10, 7),
1: (10, 7),
2: (12, 7)}.get(len(ydata) // 256, (14, 8))
self.__mpl = dict(zip(('fig', 'axx'),
plt.subplots(1, figsize=figsize)))
self.__mpl['time_axis'] = isinstance(xdata[0], datetime)
# add a centered subtitle to the figure
self.__add_caption(self.__mpl['fig'])
# set color cycle
if self.cset is None:
self.__mpl['axx'].set_prop_cycle(None)
else:
self.__mpl['axx'].set_prop_cycle(color=self.cset)
# draw line in figure
fig_draw_lplot(self.__mpl['axx'], xdata, ydata, **kwargs)
# --------------------------------------------------
[docs] def draw_multiplot(self, data_tuple: tuple, gridspec=None, *,
fig_info: FIGinfo | None = None,
title: str | None = None, **kwargs) -> None:
"""Display multiple subplots on one page using
`matplotlib.gridspec.GridSpec`.
Parameters
----------
data_tuple : tuple of np.ndarray, xarray.DataArray or xarray.Dataset
One dataset per subplot.
gridspec : matplotlib.gridspec.GridSpec, optional
Instance of `matplotlib.gridspec.GridSpec`.
fig_info : FIGinfo, optional
Meta-data to be displayed in the figure.
title : str, optional
Title of this figure using `Axis.set_title`.
Ignored when data is a `xarray` data-structure.
**kwargs : other keywords
The keywords are passed to `matplotlib.pyplot.plot`.
Ignored when data is a `xarray` data-structure.
See Also
--------
matplotlib.pyplot.plot : Plot y versus x as lines and/or markers.
Notes
-----
When data is a xarray.DataArray then the following attributes are used:
- long_name: used as the title of the main panel when parameter 'title'\
is not defined.
- units: units of the data
- _plot: dictionary with parameters for matplotlib.pyplot.plot
- _title: title of the subplot (matplotlib: Axis.set_title)
- _text: text shown in textbox placed in the upper left corner
- _yscale: y-axis scale type, default 'linear'
- _xlim: range of the x-axis
- _ylim: range of the y-axis
Examples
--------
Show two numpy arrays, each in a different panel. The subplots are
above each other (row=2, col=1). The X-coordinates are generated
using np.range(ndarray1) and np.range(ndarray2)::
> data_tuple = (ndarray1, ndarray2)
> plot = MONplot(fig_name)
> plot.draw_multiplot(data_tuple, title='my title',
... marker='o', linestyle='', color='r')
> plot.close()
Show four DataArrays, each in a different panel. The subplots
are above each other in 2 columns (row=2, col=2). The X-coordinates
are generated from the first dimension of the DataArrays::
> data_tuple = (xarr1, xarr2, xarr3, xarr4)
> plot = MONplot(fig_name)
> plot.draw_multiplot(data_tuple, title='my title',
... marker='o', linestyle='')
> plot.close()
Show the DataArrays in a Dataset, each in a different panel. If there
are 3 DataArrays preset then the subplots are above each other (row=3,
col=1). The X-coordinates are generated from the (shared?) first
dimension of the DataArrays::
> plot = MONplot(fig_name)
> plot.draw_multiplot(xds, title='my title')
> plot.close()
"""
# generate figure using contained layout
fig = plt.figure(figsize=(10, 10))
# define grid layout to place subplots within a figure
if gridspec is None:
geometry = {1: (1, 1),
2: (2, 1),
3: (3, 1),
4: (2, 2)}.get(len(data_tuple))
gridspec = GridSpec(*geometry, figure=fig)
else:
if len(data_tuple) > gridspec.nrows * gridspec.ncols:
raise RuntimeError('grid too small for number of datasets')
# determine xylabels
xylabels = get_xylabels(gridspec, data_tuple)
# add a centered subtitle to the figure
self.__add_caption(fig)
# add subplots, cycle the DataArrays of the Dataset
axx = None
data_iter = iter(data_tuple)
for iy in range(gridspec.nrows):
for ix in range(gridspec.ncols):
axx = fig.add_subplot(gridspec[iy, ix])
axx.grid(True)
data = next(data_iter)
if isinstance(data, np.ndarray):
if ix == iy == 0 and title is not None:
axx.set_title(title)
axx.plot(np.arange(data.size), data, **kwargs)
elif isinstance(data, xr.DataArray):
draw_subplot(axx, data, xylabels[iy, ix, :])
else:
for name in data.data_vars:
draw_subplot(axx, data[name], xylabels[iy, ix, :])
# add annotation and save the figure
self.__add_copyright(axx)
if fig_info is None:
fig_info = FIGinfo()
self.__add_fig_box(fig, fig_info)
self.__close_this_page(fig)
# --------------------------------------------------
[docs] def draw_tracks(self, lons: np.ndarray, lats: np.ndarray,
icids: np.ndarray, *,
saa_region: np.ndarray | None = None,
fig_info: FIGinfo | None = None,
title: str | None = None) -> None:
"""Display tracks of satellite on a world map
using a Robinson projection.
Parameters
----------
lons : (N, 2) array-like
Longitude coordinates at start and end of measurement
lats : (N, 2) array-like
Latitude coordinates at start and end of measurement
icids : (N) array-like
ICID of measurements per (lon, lat)
saa_region : (N, 2) array-like, optional
The coordinates of the vertices. When defined, then show SAA region
as a matplotlib polygon patch
fig_info : FIGinfo, optional
OrderedDict holding meta-data to be displayed in the figure
title : str, optional
Title of this figure using `Axis.set_title`
The information provided in the parameter 'fig_info' will be displayed
in a small box.
"""
if not FOUND_CARTOPY:
raise RuntimeError("You need Cartopy to use this method")
if fig_info is None:
fig_info = FIGinfo()
# define plot layout
# pylint: disable=abstract-class-instantiated
myproj = {'projection': ccrs.Robinson(central_longitude=11.5)}
fig, axx = plt.subplots(figsize=(12.85, 6), subplot_kw=myproj)
# add a centered subtitle of the Figure
self.__add_caption(fig)
# add title to image panel
if title is not None:
axx.set_title(title)
# draw tracks of satellite
fig_draw_tracks(axx, lons, lats, icids, saa_region)
# finalize figure
self.__add_copyright(axx)
self.__add_fig_box(fig, fig_info)
self.__close_this_page(fig)
# --------------------------------------------------
[docs] def draw_fov_ckd(self, data: xr.DataArray | np.ndarray, *,
vp_blocks: tuple,
vp_labels: tuple[str] | None = None,
fig_info: FIGinfo | None = None,
title: str | None = None, **kwargs) -> None:
"""Display a 2D CKD parameter which consists of data from several
viewports.
Parameters
----------
data : numpy.ndarray or xarray.DataArray
Object holding measurement data and attributes.
vp_blocks : tuple
Ranges of rows belonging to the data of one viewport. Each block
is show in a separate subplot.
vp_labels : tuple of str
Label for each viewport, default=('+50', '+20', '0', '-20', '-50').
fig_info : FIGinfo, default=None
OrderedDict holding meta-data to be displayed in the figure.
title : str, default=None
Title of this figure using `Axis.set_title`.
**kwargs : other keywords
Keyword arguments: `zscale`, `vperc` or `vrange`
Notes
-----
The current implementation only works for SPEXone CKD: FIELD_OF_VIEW,
POLARIMETRIC, RADIOMETRIC and WAVELENGTH
See also
--------
fig_data_to_xarr : Prepare image data for plotting.
Examples
--------
Read SPEXone CKD::
> from pyspex.ckd_io import CKDio
> with CKDio(ckd_file) as ckd:
> fov_ckd = ckd.fov()
> rad_ckd = ckd.radiometric()
Set row-ranges belonging to one viewport::
> nview = fov_ckd.dims['viewports']
> vp_blocks = ()
> for ii in range(nview):
> ibgn = int(fov_ckd['fov_ifov_start_vp'][nview - ii - 1])
> iend = int(ibgn + fov_ckd['fov_nfov_vp'][nview - ii - 1] + 1)
> vp_blocks += ([ibgn, iend],)
Create figures::
> from moniplot.mon_plot import MONplot
> plot = MONplot('test_spx1_fov_ckd.pdf', caption='SPEXone CKD')
> plot.draw_fov_ckd(rad_ckd.isel(polarization_directions=0),
> vp_blocks=vp_blocks,
> title=rad_ckd.attrs['long_name'] + ' (S+)')
> plot.draw_fov_ckd(rad_ckd.isel(polarization_directions=1),
> vp_blocks=vp_blocks, zscale='log',
> title=rad_ckd.attrs['long_name'] + ' (S-)')
> plot.close()
"""
if vp_labels is None:
vp_labels = ('+50', '+20', '0', '-20', '-50')
if fig_info is None:
fig_info = FIGinfo()
# convert, if necessary, input data to xarray.DataArray
if isinstance(data, xr.DataArray) and '_zscale' in data.attrs:
xarr = data.copy()
else:
xarr = fig_data_to_xarr(data, **kwargs)
# get dimensions needed to draw the data of the viewports
nview = len(vp_labels)
ncol = xarr.sizes['spectral_detector_pixels']
# define plot layout
figsize = (1.75 * (xarr.sizes['spectral_detector_pixels']
// xarr.sizes['spatial_samples_per_image']), 4.5)
fig, axs = plt.subplots(nview, 1, figsize=figsize, sharex='all')
fig.subplots_adjust(hspace=0, wspace=0,
left=0.075, right=1.05,
top=0.8)
# add a centered subtitle of the Figure
self.__add_caption(fig)
# add title to image panel
if title is not None:
axs[0].set_title(title)
elif 'long_name' in xarr.attrs:
axs[0].set_title(xarr.attrs['long_name'])
ax_img = None
cmap = self.cmap if self.cmap else xarr.attrs['_cmap']
for ii in range(nview):
axs[ii].set_anchor((.5, (nview - ii - 1) * 0.25))
ibgn, iend = vp_blocks[ii]
extent = (0, ncol, ibgn, iend)
ax_img = axs[ii].imshow(xarr.values[ibgn:iend, :],
norm=xarr.attrs['_znorm'], cmap=cmap,
extent=extent, aspect=2,
interpolation='none', origin='lower')
axs[ii].set_xticks([x * ncol // 8 for x in range(9)])
axs[ii].set_yticks([ibgn, (iend + ibgn) // 2])
yax2 = axs[ii].secondary_yaxis(-.05)
yax2.tick_params(left=False, labelleft=False)
yax2.set_ylabel(vp_labels[ii])
if ii == nview // 2:
axs[ii].set_ylabel(xarr.dims[0])
if ii == nview - 1:
axs[ii].set_xlabel(xarr.dims[1])
else:
axs[ii].tick_params(labelbottom=False)
# defaults: pad=0.05, aspect=20
fig.colorbar(ax_img, ax=axs, pad=0.01, aspect=10,
label=xarr.attrs['_zlabel'])
# finalize figure
self.__add_copyright(axs[-1])
self.__add_fig_box(fig, fig_info)
self.__close_this_page(fig)