Source code for sofia_redux.visualization.display.figure
# Licensed under a 3-clause BSD style license - see LICENSE.rst
from typing import (List, Any, Dict, Union,
Optional, TypeVar, Tuple)
import uuid
import matplotlib.axes as ma
from matplotlib import gridspec
from matplotlib import style as ms
from matplotlib import backend_bases as mbb
import numpy as np
from sofia_redux.visualization import log
from sofia_redux.visualization.signals import Signals
from sofia_redux.visualization.models import high_model, reference_model
from sofia_redux.visualization.display import pane, blitting, gallery, drawing
from sofia_redux.visualization.utils.eye_error import EyeError
from sofia_redux.visualization.utils.model_fit import ModelFit
__all__ = ['Figure']
MT = TypeVar('MT', bound=high_model.HighModel)
RT = TypeVar('RT', bound=reference_model.ReferenceData)
PT = TypeVar('PT', bound=pane.Pane)
PID = TypeVar('PID', int, str)
IDT = TypeVar('IDT', uuid.UUID, str)
[docs]
class Figure(object):
"""
Oversee the plot.
The Figure class is analogous to matplotlib.figure.Figure. It
handles all plots generated. There is only one `Figure` instance
created for the Eye. Each plot resides in a `Pane` object.
The `Figure` receives commands from the `View` object
and decides which `Pane` it applies to, as well as how it should
be formatted. The `Figure` does not manage any Qt objects.
Parameters
----------
figure_widget : QtWidgets.QWidget
The widget in the Qt window containing the matplotlib
canvas to display to.
signals : sofia_redux.visualization.signals.Signals
Custom signals recognized by the Eye interface, used
to trigger callbacks outside the Figure.
Attributes
----------
widget : QtWidgets.QWidget
The widget in the Qt window that `Figure` connects to.
fig : matplotlib.figure.Figure
The Matplotlib Figure object contained in the Qt widget.
panes : list
A list of `Pane` objects.
gs : gridspec.GridSpec
The gridspec that details how the panes are aligned and
spaced out.
signals : sofia_redux.visualization.signals.Signals
Collection of PyQt signals for passing on information to
other parts of the Eye.
_current_pane : list
List of indices of the currently selected panes in `panes`.
highlight_pane : bool
Flag to specify if the current pane should be highlighted.
layout : {'grid', 'rows', 'columns'}
Determines the layout in a plot.
color_cycle : {'spectral', 'tableau', 'accessible'}
Color cycle to set.
plot_type : {'line', 'step', 'scatter'}
Plot type to set.
show_markers : bool
If set, markers will be shown.
show_grid : bool
If set, a background grid is shown.
dark_mode : bool
If set, dark mode is enabled.
recording : bool
Status flag, indicating whether a cursor mode is active.
gallery : gallery.Gallery
Gallery object tracking plot artists in the figure.
blitter : blitting.Blitmanager
Blitting manager for the figure.
"""
def __init__(self, figure_widget, signals: Signals) -> None:
self.widget = figure_widget
self.fig = figure_widget.canvas.fig
self.panes = list()
self.gs = None
self.signals = signals
self._current_pane = list()
self.block_current_pane_signal = False
self.highlight_pane = True
self.layout = 'grid'
self.color_cycle = 'Accessible'
self.plot_type = 'Step'
self.show_markers = False
self.show_grid = False
self.show_error = True
self.dark_mode = False
self.recording = False
self._cursor_locations = list()
self._cursor_mode = None
self._cursor_pane = None
self._fit_params = list()
self.gallery = gallery.Gallery()
self.blitter = blitting.BlitManager(canvas=figure_widget.canvas,
gallery=self.gallery,
signals=signals)
@property
def current_pane(self) -> List[int]:
"""list of int : Currently active panes."""
return self._current_pane
@current_pane.setter
def current_pane(self, value: List[int]) -> None:
if not isinstance(value, list):
value = [value]
value = filter(lambda i: self.valid_pane(i, len(self.panes)), value)
self._current_pane = list(value)
if not self.block_current_pane_signal:
self.signals.current_pane_changed.emit()
[docs]
@staticmethod
def valid_pane(index: Any, pane_count: int) -> bool:
"""
Check if a pane index is valid.
Parameters
----------
index : int
Index of a pane
pane_count : int
Total number of panes
Returns
-------
bool
If the index is an int and less than pane_count, returns True
otherwise False.
"""
try:
index = int(index)
except (ValueError, TypeError):
return False
else:
if 0 <= index <= pane_count:
return True
else:
return False
[docs]
def set_pane_highlight_flag(self, state: bool) -> None:
"""
Set the visibility for a pane highlight border.
Parameters
----------
state : bool
True to show; False to hide.
"""
self.highlight_pane = state
self.gallery.set_pane_highlight_flag(pane_numbers=self.current_pane,
state=state)
[docs]
def set_block_current_pane_signal(self, value: bool = True) -> None:
"""
Set the flag to block the current_pane signal.
Parameters
----------
value : bool, optional
True to block the current_pane signal; False to allow it
to propagate.
"""
self.block_current_pane_signal = value
[docs]
def set_layout_style(self, value: str = 'grid') -> None:
"""
Set the layout style.
Parameters
----------
value : ['grid', 'rows', 'columns'], optional
The layout to set.
"""
self.layout = value
####
# Panes
####
[docs]
def populated(self) -> bool:
"""
Check for pane existence.
Returns
-------
bool
True if any panes exist, else False.
"""
return len(self.panes) > 0
[docs]
def pane_count(self) -> int:
"""
Retrieve the number of panes.
Returns
-------
int
The pane count.
"""
return len(self.panes)
[docs]
def pane_layout(self) -> Union[None, Tuple[int, int]]:
"""
Retrieve the current pane layout.
Returns
-------
geometry : tuple of int, or None
If there is an active layout, (nrow, ncol) is returned.
Otherwise, None.
"""
if self.gs is None:
return None
else:
return self.gs.get_geometry()
[docs]
def add_panes(self, n_dims: Union[int, Tuple, List],
n_panes: int = 1) -> None:
"""
Add new panes to the figure.
Parameters
----------
n_dims : int, list-like
Specifies the number of dimensions for the new panes,
which determines if they are for spectra or images. If
multiple panes are being added, this can be a single
value that will apply to all new panes, or a list-like
object that specifies the dimensions for each new pane.
n_panes : int
Number of panes to be added.
Raises
------
RuntimeError :
If inconsistent or invalid options are given.
"""
self.set_block_current_pane_signal(True)
self.fig.clear()
if n_panes == 0:
return
if not isinstance(n_dims, (tuple, list)):
n_dims = [n_dims] * n_panes
else:
if len(n_dims) != n_panes:
raise RuntimeError(f'Length of pane dimensions does not match '
f'number of panes requested: {len(n_dims)} '
f'!= {n_panes}')
for i, dimension in zip(range(n_panes), n_dims):
if dimension == 0:
new_pane = pane.OneDimPane(self.signals)
elif dimension == 1:
new_pane = pane.OneDimPane(self.signals)
elif dimension == 2:
new_pane = pane.TwoDimPane(self.signals)
else:
raise RuntimeError(f'Invalid number of dimensions for '
f'pane: {dimension}')
# set user defaults
new_pane.set_color_cycle_by_name(self.color_cycle)
new_pane.set_plot_type(self.plot_type)
new_pane.set_markers(self.show_markers)
new_pane.set_grid(self.show_grid)
new_pane.set_error(self.show_error)
new_pane.data_changed = True
self.panes.append(new_pane)
self._assign_axes()
self.reset_artists()
self.current_pane = [len(self.panes) - 1]
self.set_block_current_pane_signal(False)
self.signals.atrophy_bg_partial.emit()
def _assign_axes(self) -> None:
"""Assign axes to all current panes."""
if not self.populated():
return
self._derive_pane_grid()
for i, pane_ in enumerate(self.panes):
ax = self.fig.add_subplot(self.gs[i])
pane_.set_axis(ax)
if pane_.show_overplot:
ax_alt = ax.twinx()
ax_alt.autoscale(enable=True)
pane_.set_axis(ax_alt, kind='alt')
def _derive_pane_grid(self) -> None:
"""
Determine the gridspec to populate with panes.
Uses self.layout to determine the style (may be
'grid', 'rows', or 'columns').
"""
n_tot = len(self.panes)
if self.layout == 'rows':
n_rows = n_tot
n_cols = 1
elif self.layout == 'columns':
n_rows = 1
n_cols = n_tot
else:
n_rows = int(np.ceil(np.sqrt(n_tot)))
n_cols = int(np.ceil(n_tot / n_rows))
self.gs = gridspec.GridSpec(n_rows, n_cols, figure=self.fig)
[docs]
def remove_artists(self) -> None:
"""Remove all artists."""
self.gallery.reset_artists('all')
[docs]
def reset_artists(self) -> None:
"""Recreate all artists from models."""
self.gallery.reset_artists('all')
for pane_ in self.panes:
new_drawings = pane_.create_artists_from_current_models()
successes = 0
successes += self.gallery.add_drawings(new_drawings)
if successes != len(new_drawings):
log.debug('Error encountered while creating artists')
# add borders back in
self._add_pane_artists()
# add fits back in; managed by view
self.signals.toggle_fit_visibility.emit()
# add reference models if available
self.signals.update_reference_lines.emit()
def _add_pane_artists(self) -> None:
"""Track existing border artists."""
borders = dict()
for pane_number, pane_ in enumerate(self.panes):
if self.highlight_pane:
if (self.current_pane is not None
and self.current_pane != []
):
visible = (pane_number in self.current_pane)
else:
# catch for case where border is added before
# current pane is set, on initialization
visible = (pane_number == 0)
else:
visible = False
borders[f'pane_{pane_number}'] = {'kind': 'border',
'artist': pane_.get_border(),
'visible': visible}
self.gallery.add_patches(borders)
def _add_crosshair(self) -> None:
"""Track existing crosshair artists."""
crosshairs = list()
for pane_number, pane_ in enumerate(self.panes):
pane_lines = pane_.plot_crosshair()
for pane_line in pane_lines:
model_name = f'crosshair_pane_{pane_number}'
pane_line.set_high_model(model_name)
pane_line.set_visible(False)
crosshairs.append(pane_line)
self.gallery.add_crosshairs(crosshairs)
[docs]
def model_matches_pane(self, pane_: PID, model_: MT) -> bool:
"""
Check if a model is displayable in a specified pane.
Currently always returns True.
Parameters
----------
pane_ : Pane
The pane to test against.
model_ : Model
The model to test.
Returns
-------
bool
True if `model` can be added to `pane_`
"""
# TODO: make this work
return True
[docs]
def remove_all_panes(self) -> None:
"""Remove all panes."""
self.set_block_current_pane_signal(True)
self.fig.clear()
self.panes = list()
self.current_pane = None
self.set_block_current_pane_signal(False)
[docs]
def remove_pane(self, pane_id: Optional[List[int]] = None) -> None:
"""
Remove a specified pane.
Parameters
----------
pane_id : list of int
List of pane indices to remove.
"""
self.set_block_current_pane_signal(True)
self.fig.clear()
# delete specified pane
keep_panes = list()
for idx in range(len(self.panes)):
if pane_id is not None:
if idx not in pane_id:
keep_panes.append(self.panes[idx])
else:
if idx not in self.current_pane:
keep_panes.append(self.panes[idx])
self.panes = keep_panes
# reset current pane
if self.populated():
self._assign_axes()
self.current_pane = [len(self.panes) - 1]
else:
# no panes left - leave figure blank
self.current_pane = None
self.reset_artists()
self.set_block_current_pane_signal(False)
[docs]
def pane_details(self) -> Dict[str, Dict[str, Any]]:
"""
Compile summaries of all panes in the figure.
Returns
-------
dict
Keys are 'pane_{i}', where i is the pane index.
Values are the model summary dicts for the pane.
"""
details = dict()
for i, _pane in enumerate(self.panes):
details[f'pane_{i}'] = _pane.model_summaries()
return details
[docs]
def change_current_pane(self, pane_id: List[int]) -> bool:
"""
Set the current pane to the index provided.
Parameters
----------
pane_id : int
Index of the new current pane.
Returns
-------
changed : bool
True if current pane has changed; False if the new
current pane is the same as the old current pane.
"""
if self.current_pane == pane_id:
return False
elif len(pane_id) == 0:
self.current_pane = list()
return True
elif all([len(self.panes) > p >= 0 for p in pane_id]):
self.current_pane = pane_id
return True
else:
return False
[docs]
def determine_selected_pane(self, ax: Optional[ma.Axes] = None,
all_ax: Optional[bool] = False
) -> List[int]:
"""
Determine what pane corresponds to a provided axes.
Parameters
----------
ax : matplotlib.axes.Axes
The axes to find.
all_ax : bool
True if all axis are to be selected.
Returns
-------
index : int, None
The pane index of the correct pane, or None if the
axes were not found.
"""
if all_ax:
panes = list(range(len(self.panes)))
else:
panes = list()
for i, _pane in enumerate(self.panes):
if ax in _pane.axes():
panes.append(i)
return panes
[docs]
def determine_pane_from_model(self, model_id: str,
order: Optional[int] = None) -> List[PT]:
"""
Determine pane containing specified model.
Parameters
----------
model_id : str
Specific model_id associated with the model to be found.
order : int, optional
Specific order (aperture number) to be found.
Returns
-------
found : list[(int,sofia_redux.visualization.display.pane.Pane)]
List of panes containing given model_id and order number.
"""
found = list()
for i, pane_ in enumerate(self.panes):
if pane_.contains_model(model_id, order=order):
found.append((i, pane_))
return found
[docs]
def get_current_pane(self) -> Optional[List[PT]]:
"""
Return the current pane.
Returns
-------
pane : sofia_redux.visualization.display.pane.Pane, None
The current pane if a current pane exists;
None otherwise.
"""
if self.populated():
panes = [self.panes[i] for i in self.current_pane]
return panes
else:
return None
[docs]
def get_fields(self, target: Optional[Any] = None) -> List:
"""
Get the fields associated with a given pane selection.
Parameters
----------
target : str, None, or list of int
May be set to 'all' to apply to all panes, None
to apply only to the current pane, or a list of
pane indices to modify.
Returns
-------
fields : list
List of fields for corresponding to pane selection
provided.
"""
panes, axes = self.parse_pane_flag(target)
fields = list()
for _pane in panes:
fields.append(_pane.get_field(axis=axes))
return fields
[docs]
def get_units(self, target: Optional[Any] = None) -> List:
"""
Get the units associated with a given pane selection.
Parameters
----------
target : str, None, or list of int
May be set to 'all' to apply to all panes, None
to apply only to the current pane, or a list of
pane indices to modify.
Returns
-------
units : list
List of units for corresponding to pane selection
provided.
"""
panes, axes = self.parse_pane_flag(target)
units = list()
for _pane in panes:
units.append(_pane.get_unit(axis=axes))
return units
[docs]
def get_orders(self, target: Optional[Any] = None,
enabled_only: Optional[bool] = True,
model: Optional[MT] = None,
filename: Optional[str] = None,
model_id: Optional[IDT] = None,
group_by_model: Optional[bool] = True,
kind: Optional[str] = 'order') -> Dict:
"""
Get the orders associated with a given pane selection.
Parameters
----------
target : str, None, dict, or list of int
May be set to 'all' to apply to all panes, None
to apply only to the current pane, or a list of
pane indices to modify.
enabled_only : bool, optional
Determines if an order is going to be visible or not.
model : high_model.HighModel, optional
Target model
filename : str
Name of file
model_id : uuid.UUID
Unique UUID for an HDUL.
group_by_model : bool, optional.
If set, return a dictionary with the keys are model names
and the values are the orders for that model. Otherwise,
return a list of all model orders combined.
Returns
-------
orders : dict
Dictionary of orders for corresponding to pane selection
provided. Keys are the indices of the panes.
"""
panes, axes = self.parse_pane_flag(target)
orders = dict()
if panes:
for i, _pane in enumerate(panes):
pane_index = self.panes.index(_pane)
orders[pane_index] = _pane.get_orders(
enabled_only=enabled_only, by_model=group_by_model,
target_model=model, filename=filename,
model_id=model_id, kind=kind)
return orders
[docs]
def ap_order_state(self, target, model_ids):
"""
Get the current aperture and order configuration.
Parameters
----------
target : str, None, list of int, or list of Panes
Panes to examine.
model_ids : list of UUID
Model IDs to examine.
Returns
-------
apertures, orders : dict, dict
Keys are pane index for which the specified model was
found. Values are numbers of apertures and orders displayed,
respectively, for that pane.
"""
panes, axes = self.parse_pane_flag(target)
apertures = dict()
orders = dict()
if panes:
for i, pane_ in enumerate(panes):
pane_index = self.panes.index(pane_)
ap_count, ord_count = pane_.ap_order_state(model_ids)
orders[pane_index] = ord_count
apertures[pane_index] = ap_count
return apertures, orders
[docs]
def get_scales(self, target: Optional[Any] = None) -> List:
"""
Get the axis scales associated with a given pane selection.
Parameters
----------
target : str, None, dict, or list of int
May be set to 'all' to apply to all panes, None
to apply only to the current pane, or a list of
pane indices to modify.
Returns
-------
scales : list
List of axes scales for corresponding to pane selection
provided.
"""
log.debug(f'Getting scale for {target}.')
panes, axes = self.parse_pane_flag(target)
scales = list()
for _pane in panes:
scales.append(_pane.get_scale())
return scales
####
# Handling models
####
[docs]
def model_backup(self, models: MT, target):
"""
Obtain the backup models and assign them to panes.
Parameters
----------
models : high_model.HighModel
HighModels to be loaded into pane
target : str, None, or list of int
May be set to 'all' to apply to all panes, None
to apply only to the current pane, or a list of
pane indices to modify.
"""
panes, axes = self.parse_pane_flag(target)
for _pane in panes:
if _pane is not None:
_pane.update_model(models)
[docs]
def assign_models(self, mode: str, models: Dict[str, MT],
indices: Optional[List[int]] = None) -> int:
"""
Assign models to panes.
Parameters
----------
mode : ['split', 'first', 'last', assigned']
Specifies how to arrange the models on the panes.
'Split' divides the models as evenly as possible
across all present panes. 'First' assigns all the
models to the first pane, while 'last' assigns all
the models to the last pane. 'Assigned' attaches
each model to the pane index provided in `indices`.
models : dict
Dictionary of models to add. Keys are the model ID,
with the values being the models themselves.
indices : list of int, optional
A list of integers with the same length of `models`.
Only used for `assigned` mode. Specifies the index
of the desired pane for the model.
Raises
------
RuntimeError :
If an invalid mode is provided.
"""
errors = 0
if mode == 'split':
pane_count = self.pane_count()
model_keys = list(models.keys())
models_per_pane = self._assign_models_per_pane(
model_count=len(models), pane_count=pane_count)
for i, pane_ in enumerate(self.panes):
model_count = models_per_pane[i]
for j in range(model_count):
model_ = models[model_keys.pop(0)]
try:
self.add_model_to_pane(model_=model_, panes=pane_)
except EyeError:
errors += 1
elif mode == 'first':
for model_ in models.values():
try:
self.add_model_to_pane(model_=model_, panes=self.panes[0])
except EyeError:
errors += 1
elif mode == 'last':
for model_ in models.values():
try:
self.add_model_to_pane(model_=model_, panes=self.panes[-1])
except EyeError:
errors += 1
elif mode == 'assigned':
# TODO: This method assumes the dictionary remains ordered,
# which is the default behavior of dictionaries. However,
# it is not reliable. Change this so `indices` are also
# a dict with the same keys as models.
for model_, pane_index in zip(models.values(), indices):
try:
self.add_model_to_pane(model_=model_,
panes=self.panes[pane_index])
except EyeError:
errors += 1
else:
raise RuntimeError('Invalid mode')
return errors
@staticmethod
def _assign_models_per_pane(model_count: int,
pane_count: int) -> List[int]:
"""
Calculate the number of models per pane.
Divides the number of models evenly between the existing
panes. Used with the 'split' assignment mode.
Parameters
----------
model_count : int
The total number of models to assign.
pane_count : int
The number of panes available.
Returns
-------
models_per_pane : list of int
List with `pane_count` elements, containing the
number of models to assign to each pane.
"""
models_per_pane = [model_count // pane_count] * pane_count
remainder = model_count % pane_count
models_per_pane = [mp + 1
if i < remainder else mp
for i, mp in enumerate(models_per_pane)]
return models_per_pane
[docs]
def models_per_pane(self) -> List[int]:
"""
Retrieve the number of models in each pane.
Returns
-------
count : list of int
The number of models in each existing pane.
"""
count = [p.model_count() for p in self.panes]
return count
[docs]
def add_model_to_pane(self, model_: MT,
panes: Optional[Union[PT, List[PT]]] = None) -> None:
"""
Add model to current pane.
If there are currently no panes, create one.
If there are panes but the model is not
compatible with them, create a new one and
add the model there.
Parameters
----------
model_ : sofia_redux.visualization.models.high_model.HighModel
Model to add.
panes : sofia_redux.visualization.display.pane.Pane, Optional
A list of panes to which we add model. If not provided, add
to current pane.
"""
if panes is None:
if not self.populated():
self.add_panes(model_.default_ndims, n_panes=1)
panes = [self.panes[p] for p in self.current_pane]
elif not isinstance(panes, list):
panes = [panes]
successes = list()
for pane_ in panes:
additions = list()
if self.model_matches_pane(pane_, model_):
try:
addition = pane_.add_model(model_)
except EyeError:
pane_.remove_model(model=model_)
raise
# index = self.panes.index(pane_)
# log.warning(f'{os.path.basename(model_.filename)} '
# f'incompatible with Pane {index + 1:d}')
else:
additions.extend(addition)
else:
self.add_panes(n_dims=model_.default_ndims, n_panes=1)
for p in self.current_pane:
try:
addition = self.panes[p].add_model(model_)
except EyeError:
self.panes[p].remove_model(model=model_)
raise
else:
additions.extend(addition)
if additions:
successes.append(self.gallery.add_drawings(additions))
if successes:
log.debug('Added model to panes')
self.signals.update_reference_lines.emit()
[docs]
def remove_model_from_pane(
self, filename: Optional[str] = None,
model_id: Optional[IDT] = None,
model_: Optional[MT] = None,
panes: Optional[Union[PT, List[PT]]] = None) -> None:
"""
Remove a model from one or more panes.
The model to remove can be specified by either
its filename or the model itself.
Parameters
----------
filename : str, optional
Name of the file to remove
model_id : uuid.UUID
Unique Id associated with an HDUL
model_ : sofia_redux.visualization.spectrum.model.Model, optional
Model object to remove
panes : sofia_redux.visualization.spectrum.panes.Pane, optional
A list of pane objects to remove the
model from. If not provided, the model will
be removed from all panes.
Raises
------
RuntimeError
If neither ``filename`` nor ``model`` are
provided.
"""
if filename is None and model_ is None and model_id is None:
raise RuntimeError('Must specify which model to remove '
'with either its filename or the '
'model itself.')
if filename is not None and not isinstance(filename, list):
filename = [filename]
if model_id is not None and not isinstance(model_id, list):
model_id = [model_id]
if panes is None:
panes = self.panes
elif not isinstance(panes, list):
panes = [panes]
parsed = list()
for i, p in enumerate(panes):
if isinstance(p, int):
try:
parsed.append(self.panes[p])
except IndexError:
pass
elif not isinstance(p, pane.Pane):
pass
else:
parsed.append(p)
for _pane in parsed:
if filename:
for name in filename:
_pane.remove_model(filename=name, model=model_)
elif model_id:
for mid in model_id:
_pane.remove_model(model_id=mid, model=model_)
# trigger full artist and background regeneration
self.clear_all()
if self.recording:
self.end_cursor_records()
self.signals.atrophy_bg_full.emit()
[docs]
def update_reference_lines(self, models: RT):
"""
Remove and replot reference lines.
Parameters
----------
models : reference_model.ReferenceData
reference_model.ReferenceData objects for the reference lines.
"""
self.gallery.reset_artists(selection='reference')
if models.get_visibility('ref_line'):
for pane_ in self.panes:
additions = pane_.update_reference_data(models, plot=True)
if additions:
success = self.gallery.add_drawings(additions)
if success:
log.debug('Updated reference data')
self.signals.atrophy_bg_partial.emit()
[docs]
def model_extensions(self, model_id, pane_=None,
pane_index: Optional[int] = None) -> List[str]:
"""
Obtain an extension list of a model in a pane.
Parameters
----------
model_id : uuid.UUID
Unique model id associated with an HDUL
pane_ : sofia_redux.visualization.spectrum.panes.Pane, optional
Pane object containing the model. If not provided, all panes
will be checked.
pane_index : int, optional
Index of the pane from which model extensions are desired when
no pane has been specified.
Returns
-------
ext : list
List of extensions.
"""
if pane_ is None:
if pane_index is None:
return list()
else:
pane_ = self.panes[pane_index]
ext = pane_.model_extensions(model_id)
return ext
####
# Plotting
####
[docs]
def unload_reference_model(self, models):
"""Unload the reference model."""
# TODO: argument is unnecessary here
for pane_ in self.panes:
pane_.unload_ref_model()
[docs]
def refresh(self, bg_full: bool, bg_partial: bool) -> None:
"""
Refresh the figure canvas.
Parameters
----------
bg_full : bool
If True, the full background will be redrawn, including
the plot axes.
bg_partial : bool
If True and bg_full is False, the background will
be redrawn, but the axes will remain the same.
"""
if bg_full or bg_partial:
for _pane in self.panes:
if bg_full:
_pane.data_changed = True
else:
_pane.data_changed = False
_pane.apply_configuration()
self.blitter.update_all()
else:
self.blitter.update_animated()
[docs]
def clear_all(self) -> None:
"""Clear all artists and redraw panes."""
self.set_block_current_pane_signal(True)
self.fig.clear()
self._assign_axes()
self.reset_artists()
self.set_block_current_pane_signal(False)
self.signals.atrophy_bg_partial.emit()
[docs]
def change_axis_limits(self, limits: Dict[str, float],
target: Optional[Any] = None) -> None:
"""
Change the axis limits for specified panes.
Parameters
----------
limits : dict
Keys are 'x', 'y'. Values are [low, high] limits for
the axis.
target : str, None, dict, or list of int
May be set to 'all' to apply to all panes, None
to apply only to the current pane, or a list of
pane objects
"""
log.debug(f'Update axis limits for {target} panes to '
f'{limits}')
panes, axes = self.parse_pane_flag(target)
for _pane in panes:
if _pane is not None:
_pane.set_limits(limits)
# update reference data for new limits
ref_updates = _pane.update_reference_data()
if ref_updates is not None:
self.gallery.update_reference_data(pane_=_pane,
updates=ref_updates)
[docs]
def change_axis_unit(self, units: Dict[str, str],
target: Optional[Any] = None) -> None:
"""
Change the axis unit for specified panes.
If incompatible units are specified, the current units
are left unchanged.
Parameters
----------
units : dict
Keys are 'x', 'y'. Values are the units to convert
to.
target : str, None, dict, or list of int
May be set to 'all' to apply to all panes, None
to apply only to the current pane, or a list of
"""
panes, axes = self.parse_pane_flag(target)
changed = False
for _pane in panes:
if _pane is not None:
_pane.set_units(units, axes)
changed = True
# trigger full artist regeneration
if changed:
self.clear_all()
[docs]
def change_axis_field(self, fields: Dict[str, str],
target: Optional[Any] = None) -> None:
"""
Change the axis field for specified panes.
Parameters
----------
fields : dict
Keys are 'x', 'y'. Values are the field names to
change to.
target : str, None, list of int, or list of Pane objects
May be set to 'all' to apply to all panes, None
to apply only to the current pane, or a list of
ints or Pane objects.
"""
panes, axes = self.parse_pane_flag(target)
if panes is None:
log.debug(f'No valid panes found for (target, fields) = '
f'({target}, {fields})')
else:
if axes == 'both':
fields['y_alt'] = fields['y']
elif axes == 'alt':
fields['y_alt'] = fields.pop('y')
else:
fields.pop('y_alt', None)
for _pane in panes:
if _pane is not None:
_pane.set_fields(fields)
# trigger full artist regeneration
self.clear_all()
[docs]
def set_orders(self, orders: Dict[int, Dict[IDT, List[int]]],
enable: Optional[bool] = True,
aperture: Optional[bool] = False) -> None:
"""
Enable specified orders.
Parameters
----------
orders : dict
Keys are indices for the panes to update. Values
are dicts, with model ID keys, order list values.
enable : bool, optional
If set enable the orders, otherwise disable orders.
Defaults to True.
aperture : bool, optional
If set the order numbers in `orders` are actually
aperture numbers. Defaults to False.
"""
for pane_id, pane_orders in orders.items():
pane_ = self.parse_pane_flag([pane_id])
pane_ = pane_[0]
if pane_ is None:
continue
elif isinstance(pane_, list):
pane_ = pane_[0]
updates = pane_.set_orders(pane_orders, enable, aperture,
return_updates=True)
self.gallery.update_artist_options(pane_=pane_, options=updates)
[docs]
def set_scales(self, scales: Dict[str, str],
target: Optional[Any] = None) -> None:
"""
Set the axis scale for specified panes.
Parameters
----------
scales : dict
Keys are 'x', 'y'. Values are 'linear' or 'log'.
target : str, None, list of int, or list of Pane objects
May be set to 'all' to apply to all panes, None
to apply only to the current pane, or a list of
ints or Pane objects.
"""
panes, axes = self.parse_pane_flag(target)
if panes is None:
return
if axes in ['both', 'all']:
scales['y_alt'] = scales['y']
elif axes == 'alt':
scales['y_alt'] = scales.pop('y')
else:
scales.pop('y_alt', None)
for _pane in panes:
if _pane is not None:
_pane.set_scales(scales)
[docs]
def set_overplot_state(self, state: bool, target: Optional[Any] = None
) -> None:
"""
Set the pane overplot flag.
Parameters
----------
state : bool
True to show overplot; False to hide
target : str, None, or list of int
May be set to 'all' to apply to all panes, None
to apply only to the current pane, or a list of
pane indices to modify.
"""
panes, axes = self.parse_pane_flag(target)
if state: # Turning on
for _pane in panes:
if _pane is not None:
_pane.set_overplot(state)
self.reset_artists()
else: # Turning off
self.gallery.reset_artists(selection='alt', panes=panes)
for _pane in panes:
if _pane is not None:
_pane.reset_alt_axes(remove=True)
_pane.set_overplot(state)
# trigger full regeneration: some things get orphaned
# when axes change (eg. border artist)
self.clear_all()
[docs]
def parse_pane_flag(self, flags: Optional[Union[List, Dict,
List[Union[int, PID]]]]
) -> Tuple[List[PT], str]:
"""
Parse the specified panes from an input flag.
Parameters
----------
flags : str, None, list of int, or list of Panes
May be set to 'all' to apply to all panes, None
to apply only to the current pane, or a list of
ints or Pane objects.
Returns
-------
panes, axis : list, str
List of panes corresponding to input flag and corresponding axis
"""
log.debug(f'Parsing {flags} ({type(flags)})')
axis = ''
panes = None
if flags is None:
panes = self.get_current_pane()
elif flags == 'all':
panes = self.panes
elif isinstance(flags, int):
try:
panes = [self.panes[flags]]
except IndexError:
log.debug(f'Unable to parse pane flag {flags}: '
f'Invalid index')
elif isinstance(flags, list):
if all([isinstance(p, int) for p in flags]):
panes = list(map(self.panes.__getitem__, flags))
elif not all([isinstance(p, pane.Pane) for p in flags]):
raise TypeError('List of panes can only contain '
'integers or Pane objects.')
else:
panes = flags
elif isinstance(flags, dict):
try:
pane_flags = flags['pane']
except KeyError:
raise EyeError(f'Unable to parse pane flag {flags}')
else:
if pane_flags == 'all':
panes = self.panes
elif pane_flags == 'current':
panes = [self.get_current_pane()]
elif isinstance(pane_flags, list):
panes = list()
for flag in pane_flags:
if isinstance(flag, pane.Pane):
panes.append(flag)
elif isinstance(flag, int):
try:
panes.append(self.panes[flag])
except IndexError:
log.debug(f'Invalid pane flag: {flag}')
continue
else:
log.debug(f'Invalid pane flag: {flag}')
continue
else:
raise EyeError(f'Unable to parse pane flag {flags}')
axis = flags.get('axis', axis)
log.debug(f'Parsed {panes} ({type(panes)})')
return panes, axis
[docs]
def set_color_cycle(self, cycle_name: str) -> None:
"""
Set the color cycle in all panes.
Parameters
----------
cycle_name: ['spectral', 'tableau', 'accessible']
Color cycle to set.
"""
self.color_cycle = cycle_name
full_updates = dict()
if self.populated():
for i, pane_ in enumerate(self.panes):
pane_.set_color_cycle_by_name(cycle_name)
updates = pane_.update_colors()
self.gallery.update_artist_options(pane_=pane_,
options=updates)
full_updates[i] = updates
self.signals.atrophy.emit()
return full_updates
[docs]
def set_plot_type(self, plot_type: str) -> None:
"""
Set the plot type in all panes.
Parameters
----------
plot_type: ['line', 'step', 'scatter']
Plot type to set.
"""
self.plot_type = plot_type
if self.populated():
for pane_ in self.panes:
line_updates = pane_.set_plot_type(plot_type)
self.gallery.update_line_type(pane_=pane_,
updates=line_updates)
self.signals.atrophy.emit()
[docs]
def set_markers(self, state: bool = True) -> None:
"""
Set the marker visibility in all panes.
Parameters
----------
state: bool, optional
If True, markers will be shown. If False, they
will be hidden.
"""
self.show_markers = state
if self.populated():
for pane_ in self.panes:
marker_updates = pane_.set_markers(state)
self.gallery.update_artist_options(pane_=pane_,
options=marker_updates)
self.signals.atrophy.emit()
[docs]
def get_markers(self, model_id, pane_):
"""
Get the markers in a pane.
Parameters
----------
model_id : uuid.UUID
Unique id associated with an HDUL
pane_ : sofia_redux.visualization.display.pane.Pane
The pane object from which markers are to be obtained.
Returns
-------
markers : list
A list of markers in a pane for a model_id
"""
panes, _ = self.parse_pane_flag(pane_)
markers = list()
for pane_ in panes:
markers.extend(pane_.get_marker(model_id))
return markers
[docs]
def get_colors(self, model_id, pane_):
"""
Get the colors in a pane.
Parameters
----------
model_id : uuid.UUID
Unique id associated with an HDUL
pane_ : sofia_redux.visualization.display.pane.Pane
The pane object from which colors are to be obtained.
Returns
-------
colors : list
A list of colors in a pane for a model_id
"""
panes, _ = self.parse_pane_flag(pane_)
colors = list()
for pane_ in panes:
colors.extend(pane_.get_color(model_id))
return colors
[docs]
def set_grid(self, state: bool = True) -> None:
"""
Set the grid visibility in all panes.
Parameters
----------
state: bool, optional
If True, gridlines will be shown. If False, they
will be hidden.
"""
self.show_grid = state
if self.populated():
for pane_ in self.panes:
pane_.set_grid(state)
self.signals.atrophy_bg_partial.emit()
[docs]
def set_error(self, state: bool = True) -> None:
"""
Set the error range visibility in all panes.
Parameters
----------
state: bool, optional
If True, error ranges will be shown. If False, they
will be hidden.
"""
self.show_error = state
if self.populated():
for pane_ in self.panes:
pane_.set_error(state)
updates = pane_.update_visibility(error=True)
self.gallery.update_artist_options(pane_=pane_,
options=updates)
self.signals.atrophy.emit()
[docs]
def set_dark_mode(self, state: bool = True) -> None:
"""
Set a dark background in all panes.
Parameters
----------
state: bool, optional
If True, dark mode will be enabled. If False, it
will be disabled.
"""
self.dark_mode = state
if self.dark_mode:
ms.use('dark_background')
self.fig.set_facecolor('black')
else:
ms.use('default')
self.fig.set_facecolor('white')
self.clear_all()
[docs]
def set_enabled(self, pane_id: int, model_id: str,
state: bool) -> None:
"""
Enable or disable a specified model.
Parameters
----------
pane_id : int
Pane ID to update.
model_id : str
Model ID to modify.
state : bool
If True, model will be enabled (shown). If False,
model will be disabled (hidden).
"""
pane_ = self.panes[pane_id]
pane_.set_model_enabled(model_id, state)
updates = pane_.update_visibility()
self.gallery.update_artist_options(pane_=pane_, options=updates)
self.signals.atrophy.emit()
[docs]
def set_all_enabled(self, pane_id: int, state: bool) -> None:
"""
Enable or disable all models in a pane.
Parameters
----------
pane_id : int
Pane ID to update.
state : bool
If True, models will be enabled (shown). If False,
models will be disabled (hidden).
"""
pane_ = self.panes[pane_id]
pane_.set_all_models_enabled(state)
updates = pane_.update_visibility()
self.gallery.update_artist_options(pane_=pane_, options=updates)
self.signals.atrophy.emit()
####
# Saving
####
[docs]
def save(self, filename: str, **kwargs) -> None:
"""
Save the current figure to a file.
Parameters
----------
filename : str
Full file path to save the image to.
kwargs : dict, optional
Additional keyword arguments to pass to
`matplotlib.figure.Figure.savefig`.
"""
initial_params = self._font_sizes()
fontsize = kwargs.get('fontsize', 40)
self._font_sizes(fontsize)
self.fig.savefig(filename, **kwargs)
self._font_sizes(initial_params)
def _font_sizes(self, sizes: Optional[Union[int, float, Dict]] = None
) -> Union[None, Dict]:
"""
Set font sizes for plot image.
New font sizes are set directly in the pane axes.
Parameters
----------
sizes : int, float, or dict, optional
Base size(s) to start with.
Returns
-------
initial_params : dict or None
Starting sizes for axis_label and tick_label, before
update.
"""
if sizes is None:
initial_params = {'axis_label': list(), 'tick_label': list()}
else:
initial_params = None
for pane_ in self.panes:
labels = [pane_.ax.xaxis.label, pane_.ax.yaxis.label]
tick_labels = [pane_.ax.get_xticklabels(),
pane_.ax.get_yticklabels()]
for i, label in enumerate(labels):
if sizes is None:
initial_params['axis_label'].append(label.get_fontsize())
else:
if isinstance(sizes, (int, float)):
size = sizes
else:
size = sizes['axis_label'][i]
label.set_fontsize(size)
for i, tick in enumerate(tick_labels):
if sizes is None:
initial_params['tick_label'].append(tick[0].get_fontsize())
else:
if isinstance(sizes, (int, float)):
size = sizes
else:
size = sizes['tick_label'][i]
for t in tick:
t.set_fontsize(size)
return initial_params
####
# Mouse events
####
[docs]
def data_at_cursor(self, event: mbb.MouseEvent) -> Dict:
"""
Retrieve the plot data at the cursor position.
Parameters
----------
event : matplotlib.backend_bases.MouseEvent
Mouse motion event.
Returns
-------
data_point : dict
Keys are filenames; values are lists of dicts
containing 'order', 'bin', 'bin_x', 'bin_y',
'x_field', 'y_field', 'color', and 'visible'
values for the displayed models.
"""
pane_indexes = self.determine_selected_pane(event.inaxes)
data_points = dict()
for pane_index in pane_indexes:
data_points.update(self.panes[pane_index].data_at_cursor(event))
self.gallery.update_marker(data_points)
return data_points
[docs]
def crosshair(self, event: mbb.MouseEvent) -> None:
"""
Display a crosshair at the cursor position.
Parameters
----------
event : matplotlib.backend_bases.MouseEvent
Mouse motion event.
"""
pane_index = self.determine_selected_pane(event.inaxes)
if isinstance(pane_index, list):
if len(pane_index) > 0:
pane_index = pane_index[0]
else:
pane_index = None
if (pane_index is not None
and self._cursor_pane is not None
and pane_index in self._cursor_pane):
data_point = self.panes[pane_index].xy_at_cursor(event)
direction = self._parse_cursor_direction(mode='crosshair')
self.gallery.update_crosshair(pane_index, data_point=data_point,
direction=direction)
self.signals.atrophy.emit()
[docs]
def clear_crosshair(self) -> None:
"""Clear any displayed crosshairs."""
self.gallery.reset_artists(selection='crosshair')
[docs]
def reset_data_points(self) -> None:
"""Reset any displayed cursor markers."""
if self.populated():
self.gallery.hide_cursor_markers()
[docs]
def reset_zoom(self, all_panes: Optional[bool] = False,
targets: Optional[Dict] = None) -> None:
"""
Reset axis limits to defaults.
Parameters
----------
all_panes : bool, optional
If True, all axes will be reset. Otherwise, only
the current pane will be reset.
targets : dict, optional
Specific panes to reset, specified as a list of int
under the 'pane' key in the input dictionary.
"""
if not self.populated():
return
if all_panes:
panes = self.panes
elif targets:
try:
pane_numbers = targets['pane']
except KeyError: # pragma: no cover
# missing 'pane' in targets - shouldn't happen under
# normal circumstances
panes = [self.panes[p] for p in self.current_pane]
else:
panes = [self.panes[p] for p in pane_numbers]
else:
panes = [self.panes[p] for p in self.current_pane]
# Don't want to include reference data in relim
for _pane in panes:
self.gallery.reset_artists(selection='reference', panes=[_pane])
_pane.reset_zoom()
ref_artists = _pane.update_reference_data()
if ref_artists is not None:
self.gallery.update_reference_data(pane_=_pane,
updates=ref_artists)
self.signals.atrophy_bg_partial.emit()
[docs]
def set_cursor_mode(self, mode: str) -> None:
"""
Set the cursor mode.
Cursor modes are used to manage zoom and feature fit
interactions.
Parameters
----------
mode : ['x_zoom', 'y_zoom', 'b_zoom', 'fit', '']
The mode to set.
"""
self._fit_params = list()
self._cursor_mode = mode
self._cursor_locations = list()
self.clear_crosshair()
self._cursor_pane = None
if mode != '':
self.recording = True
self._cursor_pane = self._current_pane
self._add_crosshair()
else:
self.recording = False
self.signals.atrophy_bg_partial.emit()
[docs]
def record_cursor_location(self, event: mbb.MouseEvent) -> None:
"""
Store the current cursor location.
Depending on the current user interaction in zoom or fit mode,
either a guide is displayed at the location (first click), or
all guides are cleared and the stored cursor locations
are passed to the `end_cursor_records` method (second click).
Parameters
----------
event : matplotlib.backend_bases.MouseEvent
Mouse motion event.
"""
pane_index = self.determine_selected_pane(event.inaxes)[0]
if pane_index in self._cursor_pane:
location = self.panes[pane_index].xy_at_cursor(event)
if None in location: # pragma: no cover
# could happen if overplot data is present and
# doesn't exactly match plot data
return
self._cursor_locations.append(location)
if len(self._cursor_locations) == 2:
self.end_cursor_records()
else:
# after the first click, make sure the next click is
# in the same pane
# otherwise, weirdness happens with 'All' pane button
# and mismatched pane displays
self._cursor_pane = [pane_index]
guide_drawings = self.panes[pane_index].plot_guides(
location, kind=self._parse_cursor_direction())
self.gallery.add_drawings(guide_drawings)
self.signals.atrophy.emit()
[docs]
def end_cursor_records(self, pane_index: Optional[int] = None) -> None:
"""
Complete zoom or fit interactions.
User specified locations are used to either set axis
limits or else display a new fit to a plot feature.
Parameters
----------
pane_index : int
Index of the pane to update.
"""
if pane_index is None:
pane_index = self.current_pane
if not isinstance(pane_index, list): # pragma: no cover
# this should not be reachable
pane_index = [pane_index]
if len(self._cursor_locations) == 2:
for index in pane_index:
if 'zoom' in self._cursor_mode:
self._end_zoom(index)
elif 'fit' in self._cursor_mode:
self._end_fit(index)
# reset cursor, but not fit params -- is needed for the
# cursor recording
self._cursor_mode = ''
self._cursor_locations = list()
self.recording = False
self.signals.atrophy.emit()
# this signal will clear guides
self.signals.end_cursor_recording.emit()
# this will trigger clear_selection
self.signals.end_zoom_mode.emit()
def _parse_cursor_direction(self, mode: str = 'zoom') -> str:
"""
Parse the cursor direction from the cursor mode.
Parameters
----------
mode : ['zoom', 'crosshair'], optional
If crosshair, possible directions are 'v', 'h', or 'vh'.
If zoom, possible directions are 'x', 'y', or 'b'.
Defaults to 'zoom'
Returns
-------
direction : str
The crosshair or guide direction corresponding to
the current cursor mode.
"""
if 'zoom' in self._cursor_mode:
direction = self._cursor_mode.split('_')[0]
elif 'fit' in self._cursor_mode:
direction = 'x'
else:
direction = 'b'
if mode == 'crosshair':
if direction == 'x':
direction = 'v'
elif direction == 'y':
direction = 'h'
else:
direction = 'hv'
return direction
def _end_zoom(self, pane_index: int,
direction: Optional[str] = None) -> None:
"""
Finish zoom interaction.
Parameters
----------
pane_index : int
Index of the pane to update.
direction : ['x', 'y', 'b']
If not provided, will be determined from the current
cursor mode.
"""
if direction is None:
direction = self._parse_cursor_direction()
# perform zoom
if len(self._cursor_locations) == 2:
self.panes[pane_index].perform_zoom(
zoom_points=self._cursor_locations, direction=direction)
else:
log.debug('Cancelling zoom')
self._cursor_locations = list()
# clear all h and v guides
self.gallery.reset_artists(selection='h_guide',
panes=self.panes[pane_index])
self.gallery.reset_artists(selection='v_guide',
panes=self.panes[pane_index])
# update reference data for new limits
ref_updates = self.panes[pane_index].update_reference_data()
if ref_updates is not None:
self.gallery.update_reference_data(pane_=self.panes[pane_index],
updates=ref_updates)
def _end_fit(self, pane_index: int) -> None:
"""
Finish the feature fit interaction.
Parameters
----------
pane_index : int
Index of the pane to update.
"""
if len(self._cursor_locations) != 2:
return
# sort limits by x before performing fit
if self._cursor_locations[0][0] <= self._cursor_locations[1][0]:
limits = self._cursor_locations
else:
limits = [self._cursor_locations[1], self._cursor_locations[0]]
fit_drawings, fit_params = self.panes[pane_index].perform_fit(
self._cursor_mode, limits)
self.gallery.add_drawings(fit_drawings)
self._fit_params.extend(fit_params)
[docs]
def get_selection_results(self) -> List[ModelFit]:
"""
Retrieve feature fit parameters.
Returns
-------
fit_params : list of ModelFit
Models fit to spectral selections.
"""
return self._fit_params
[docs]
def clear_lines(self, flags: Union[str, List[str]],
all_panes: Optional[bool] = False) -> None:
"""
Clear all displayed guides.
Parameters
----------
flags : str
Type of guides to clear.
all_panes : bool, optional
If True, all panes will be updated. Otherwise,
only the current pane will be updated.
"""
if not self.populated():
return
if all_panes:
panes = self.panes
else:
panes = [self.panes[p] for p in self.current_pane]
if not isinstance(flags, list):
flags = [flags]
for flag in flags:
if flag == 'fit':
self.gallery.reset_artists(flag, panes=panes)
else:
self.gallery.reset_artists(f'{flag}_guide', panes=panes)
[docs]
def toggle_fits_visibility(self, fits: List[ModelFit]) -> None:
"""
Update fit artist visibility.
If failure, loop over panes. Ask each one to make new
drawing for fit. If model, order, fields don't match,
pane does nothing. Else remake fit drawing and return them.
Then add new drawing to Gallery.
Parameters
----------
fits : list of ModelFit
Models fit to spectral selections.
"""
for fit in fits:
options = {'high_model': fit.get_filename(), 'kind': 'fit',
'model_id': fit.get_model_id(),
'mid_model': f'{fit.get_order()}.{fit.get_aperture()}',
'data_id': fit.get_id(),
'updates': {'visible': fit.get_visibility()}}
options = [drawing.Drawing(**options)]
for pane_ in self.panes:
if fit.get_axis() in pane_.axes():
result = self.gallery.update_artist_options(
pane_, kinds='fit', options=options)
if not result:
self._regenerate_fit_artists(pane_, fit, options)
elif (fit.get_fields('x') == pane_.get_field('x')
and fit.get_fields('y') == pane_.get_field('y')):
self._regenerate_fit_artists(pane_, fit, options)
[docs]
def stale_fit_artists(self, fits: List[ModelFit]):
"""
Recreate fit artists for a list of model fits.
Removes all Drawing instances for fits on a given pane and makes
them anew. It accommodates any changes (unit, scale, etc) that doesn't
change the data at all but does invalidate the current artists
(which are now marked "stale").
Parameters
----------
fits : list of ModelFit
Models fit to spectral selections.
"""
matching_panes = self._panes_matching_model_fits(fits)
for pane_idx, fits in matching_panes.items():
pane_ = self.panes[pane_idx]
self.gallery.reset_artists(selection='fit', panes=[pane_])
for fit in fits:
self._regenerate_fit_artists(pane_, fit)
def _regenerate_fit_artists(self, pane_, fit, options=None):
"""
Assign fit artists to a pane.
Parameters
----------
fit : ModelFit
Models fit to spectral selections.
pane_ : pane.Pane
Pane object to add fit artists to.
"""
try:
fit_drawings = pane_.generate_fit_artists(fit)
except (KeyError, IndexError):
# can happen if model id no longer exists
return
self.gallery.add_drawings(fit_drawings)
if options:
self.gallery.update_artist_options(pane_, kinds='fit',
options=options)
def _panes_matching_model_fits(self, fits: List[ModelFit]
) -> Dict[int, List[ModelFit]]:
"""
Get matching panes for a given list of model fits.
Parameters
----------
fits : list of ModelFit
Models fit to spectral selections.
Returns
-------
matching_panes : dict
The key is the index of the pane and values are list of all the
Modelfits in that pane.
"""
matching_panes = dict()
for fit in fits:
for idx, pane_ in enumerate(self.panes):
if fit.get_axis() in pane_.axes():
if idx in matching_panes:
matching_panes[idx].append(fit)
else:
matching_panes[idx] = [fit]
return matching_panes