Source code for devctrl.data.plot

"""Plot data using matplotlib."""

from matplotlib.axes import Axes
import numpy as np
import matplotlib.pyplot as plt
from .data import NDArray, Data, Columns
from typing import Optional, Callable

import logging
log = logging.getLogger(__name__)


DEFAULT_COLOR = "#000000"
DEFAULT_SCATTER_IN_COLORS_DOT_KW = dict(color="black", marker=".", s=8)
DEFAULT_SCATTER_IN_COLORS_KW = dict(marker="o", s=12)
[docs] def plot_data_on_ax(data: NDArray, columns: Columns, ax: Axes, key: str, xkey: str, ylabel: str|bool=True, scale_factor: None|float|int=None, error_shade=False, data_getter_obj=None, is_normalized=False, mode=None, max_ylim: None|tuple[float,float]=None, zero_hline=None, zero_hline_keys: Optional[list[str]]=None, scatter_in_colors: Optional[list[str]]=None, scatter_in_colors_kw: dict=DEFAULT_SCATTER_IN_COLORS_KW, scatter_in_colors_dot_kw: Optional[dict]=DEFAULT_SCATTER_IN_COLORS_DOT_KW, **plot_kw ): """ Plot column data on a single axis :param data: Numpy array with <columns> :param ax: Matplotlib axes :param key: Which key of <columns> to plot :param columns: List of column keys :param xkey: Which key of <columns> to plot on the x-axis (wl or E) :param ylabel: Either a label (str) or True to use the PrsData.get_column_tex_label_with_unit_and_scale() or False to not use a label :param scale_factor: Which scaling to apply to the ylabel. :param error_shade: Whether to include the errors by drawing a semi-transparent shade around the plot line :param plot_kw: Keyword arguments to pass to the axes.plot() function :param mode: Whether to use reflection or transmission labels :param zero_hline: Whether to plot a horizontal line at zero. If None, automatically plots one for certain keys :param zero_hline_keys: List of column keys for which to automatically plot a zero hline :param scatter_in_colors: If given, scatter the data in the given colors instead of plotting the line :param scatter_in_colors_kw: Keywords of the scatter plots. Colors are taken from scatter_in_colors list :param scatter_in_colors_dot_kw: If not None, scatter single-colored dots in addition to the colored dots """ if data_getter_obj is None: data_getter_obj = Data if zero_hline is None: zero_hline = key in zero_hline_keys if zero_hline == True: ax.axhline(0, color="black", linewidth=plt.rcParams["axes.linewidth"]) x_idx = columns.index(xkey) data_idx = columns.index(key) ydata = data[:, data_idx] # # apply scaling # if scale_factor is not None: # log.debug(f"Applying scale: {scale_factor} to key '{key}'") # ydata = ydata.copy()/scale_factor # ydata = remove_outliers(ydata, threshold=0.5) # ydata = data[:, data_idx] ylim_before = ax.get_ylim() if error_shade: if f"s{key}" in columns: sdata_idx = columns.index(f"s{key}") sydata = data[:, sdata_idx] # if scale_factor is not None: # sydata = sydata.copy()/scale_factor # determine the color for the shade, use semi-transparent color of line linecolor = plot_kw["color"] if "color" in plot_kw else data_getter_obj.get_column_color(key, default=DEFAULT_COLOR, mode=mode) color = "#00000030" if not linecolor.startswith("#"): # if a named color was given, try getting the #rrggbb try: import matplotlib.colors linecolor = matplotlib.colors.get_named_colors_mapping()[linecolor] if not type(linecolor) == str: log.debug(f"Non-string color: {linecolor}") linecolor = color raise ValueError() except: pass if linecolor.startswith("#"): if len(linecolor) in [7,9]: # #rrggbb(aa) color = linecolor[:7] + "33" # make semi-transparent elif len(linecolor) in [4,5]: # #rgb(a): color = linecolor[:4] + "3" # make semi-transparent ax.fill_between(data[:, x_idx], ydata-sydata, ydata+sydata, color=color) else: log.warning(f"Can not draw error shade, since 's{key}' is not in columns") if scatter_in_colors is not None: line = [] for i in range(ydata.size): collection_color = ax.scatter(data[i, x_idx], ydata[i], **(dict(color=scatter_in_colors[i]) | scatter_in_colors_kw)) if scatter_in_colors_dot_kw is not None: collection_dot = ax.scatter(data[i, x_idx], ydata[i], **(scatter_in_colors_dot_kw)) line.append(collection_dot) line.append(collection_color) else: line, = ax.plot(data[:, x_idx], ydata, **(dict(color=data_getter_obj.get_column_color(key, default=DEFAULT_COLOR, mode=mode)) | plot_kw)) if error_shade: # set y limits # if the error_shade is at some point much larger than the line, we dont want to show it (likely an outlier) # therefore we ignore the error shade ylimits ymin = np.nanmin(ydata) ymax = np.nanmax(ydata) # extra margins: 5% each side margin = 0.05 * (ymax-ymin) # if the limits were previously larger, dont re-set them (might be form another plot line) # defaults are (0.0, 1.0), ignore those default_lim = ylim_before[0] == 0.0 and ylim_before[1] == 1.0 ymin = min(ymin-margin, ylim_before[0]) if not default_lim else ymin-margin ymax = max(ymax+margin, ylim_before[1]) if not default_lim else ymax+margin if not (np.isnan(ymin) or np.isinf(ymin) or np.isnan(ymax) or np.isinf(ymax)): ax.set_ylim(ymin, ymax) else: log.debug(f"Not setting limits since at least one is invalid: ymin={ymin}, ymax={ymax}") if max_ylim: # hard limit to given values ylims = ax.get_ylim() ymin = max(ylims[0], max_ylim[0]) ymax = min(ylims[1], max_ylim[1]) ax.set_ylim(ymin, ymax) # label if ylabel == True: # if True, use auto ylabel if is_normalized: ylabel = data_getter_obj.get_column_tex_label(key, mode=mode) elif scale_factor == False: ylabel = data_getter_obj.get_column_tex_label_with_unit(key, mode=mode) else: ylabel = data_getter_obj.get_column_tex_label_with_unit_and_scale(key, scale=scale_factor, mode=mode) if ylabel: # if string ax.set_ylabel(ylabel) return line