Source code for lstein.base.LSteinPanel

#%%imports
import matplotlib.pyplot as plt
import numpy as np
from typing import Any, Dict, List, Literal, Tuple

# from .LSteinCanvas import LSteinCanvas   #no import because leads to circular import
from ..utils import minmaxscale, polar2cart, cart2polar

import logging
logger = logging.getLogger(__name__)

#%%classes
[docs] class LSteinPanel: """represents a single panel in the `LSteinCanvas` - class defining a panel sitting within a `LSteinCanvas` Attributes - `LSC` -- see `__init__()` - `theta` -- see `__init__()` - `yticks` -- see `__init__()` - `panelsize` -- see `__init__()` - `show_panelbounds` -- see `__init__()` - `show_yticks` -- see `__init__()` - `y_projection_method` -- see `__init__()` - `ytickkwargs` -- see `__init__()` - `yticklabelkwargs` -- see `__init__()` - `panelboundskwargs` -- see `__init__()` Inferred Attributes - `ylims_data` - `Tuple[float,float]` - axis limits applied to `y` - `ylimrange_data` - `float` - range of y-values - convenience field for relative definitions of plot elements - `ylims_plot` - `Tuple[float,float]` - limits used to plot the y-axis - sets the frame of reference for plotting - set to `(0,1)` for consistent results - `ylimrange_data` - `float` - range of plot values - convenience field for relative definitions of plot elements - `panel_drawn` - `bool` - flag denoting if the panel has been drawn already - to prevent drawing the panel several times when plotting - `dataseries` - `List[Dict[str,Any]]` - dataseries to be used for plotting - contains - `x` - `np.ndarray` - LSteinCanvastransformed (projected) dataseries in cartesian coordinates - ready to be plotted - `y` - `np.ndarray` - transformed (projected) dataseries in cartesian coordinates - ready to be plotted - `x_cut` - `np.ndarray` - original dataseries with axis-limits applied - `y_cut` - `np.ndarray` - original dataseries with axis-limits applied - `seriestype` - `Literal["scatter","line"]` - kind of the series to be displayed - used for implementation of plotting functions in backends - `kwargs` - kwargs to be passed to the respective plotting function in the backend Methods - `get_thetabounds()` - `get_rbounds()` - `set_yticks()` - `draw()` - `apply_axis_limits()` - `project_xy_theta()` - `project_xy_y()` - `project_xy()` - `plot()` - `scatter()` Dependencies - `matplotlib` - `numpy` - `typing` """
[docs] def __init__(self, LSC,#:LSteinCanvas, theta:float, yticks:Tuple[List[float],List[Any]], panelsize:float=np.pi/8, show_panelbounds:bool=False, show_yticks:bool=True, y_projection_method:Literal["theta","y"]="theta", ytickkwargs:dict=None, yticklabelkwargs:dict=None, panelboundskwargs:dict=None, ): """constructor - initializes class - computes inferred attributes Parameters - `LSC` - `LSteinCanvas` - parent canvas the panel is associated with - `theta` - `float` - theta value the panel is associated with - equivalent to 2.5th dimension of the dataset - similar to `pos` in `fig.add_subolot(pos)` - determines where on `LSC` the panel will be located - created panel will be centered around `theta` with a width of `panelsize` - `yticks` `Tuple[List[float],List[Any]]` - ticks to draw for the y-axis - also defines axis limits applied to `y` - i.e., bounds of the respective panel - `yticks[0][0]` corresponds to the start of the panel - `yticks[0][-1]` corresponds to the end of the panel - `yticks[0]` will be used as tickpositions - `yticks[1]` will be used as ticklabels - `yticks[0]` has to be sorted in ascending or descending order - to invert the y-axis pass `yticks[0]` in a reverse sorted manner - `panelsize` - `float`, optional - (angular) space the created panel will occupy - in radians - the entire Canvas can allocate `(thetaguidelims[1]-thetaguidelims[0])/panelsize` evenly distributed, nonoverlapping panels - the default is `np.pi/8` - `show_panelbounds` - `bool`, optional - whether to show bounds of the individual panels when rendering - the default is `False` - `show_yticks` - `bool`, optional - whether to show ticks and gridlines for y-values - the default is `True` - `y_projection_method` - `Literal["theta","y"]`, optional - method to use for the projection - the default is `theta` - uses `LSteinPanel.project_xy_theta()` - `ytickkwargs` - `dict`, optional - kwargs to pass to `ax.plot()` when drawing yticks (lines in radial direction) - used for styling - the default is `None` - will be set to `dict(c=plt.rcParams["grid.color"], ls=plt.rcParams["grid.linestyle"], lw=plt.rcParams["grid.linewidth"])` - `yticklabelkwargs` - `dict`, optional - kwargs to pass to `ax.annotate()` calls used for defining the ticklabels of the y-axis - used for styling - `pad` determines the padding w.r.t. the ticks - the default is `None` - will be set to `dict(c=plt.rcParams["axes.labelcolor"], ha="center", va="center", pad=0.1)` - `panelboundskwargs` - `dict`, optional - kwargs to pass to `ax.plot()` when drawing bounds of each panel - used for styling - the default is `None` - will be set to `dict(c=plt.rcParams["axes.edgecolor"])` Raises Returns """ self.LSC = LSC self.theta = theta self.yticks = (np.array(yticks[0]),yticks[1]) self.panelsize = panelsize self.show_panelbounds = show_panelbounds self.show_yticks = show_yticks self.y_projection_method= y_projection_method self.ytickkwargs = dict(c=plt.rcParams["grid.color"], ls=plt.rcParams["grid.linestyle"], lw=plt.rcParams["grid.linewidth"]) if ytickkwargs is None else ytickkwargs if "c" not in self.ytickkwargs.keys(): self.ytickkwargs["c"] = plt.rcParams["grid.color"] if "ls" not in self.ytickkwargs.keys(): self.ytickkwargs["ls"] = plt.rcParams["grid.linestyle"] if "lw" not in self.ytickkwargs.keys(): self.ytickkwargs["lw"] = plt.rcParams["grid.linewidth"] self.yticklabelkwargs = dict(c=plt.rcParams["axes.labelcolor"], ha="center", va="center", pad=0.1) if yticklabelkwargs is None else yticklabelkwargs if "c" not in self.yticklabelkwargs.keys(): self.yticklabelkwargs["c"] = plt.rcParams["axes.labelcolor"] if "ha" not in self.yticklabelkwargs.keys(): self.yticklabelkwargs["ha"] = "center" if "va" not in self.yticklabelkwargs.keys(): self.yticklabelkwargs["va"] = "center" if "pad" not in self.yticklabelkwargs.keys(): self.yticklabelkwargs["pad"] = 0.1 self.panelboundskwargs = dict(c=plt.rcParams["axes.edgecolor"]) if panelboundskwargs is None else panelboundskwargs if "c" not in self.panelboundskwargs.keys():self.panelboundskwargs["c"] = plt.rcParams["axes.edgecolor"] #inferred attributes self.ylims_data = (self.yticks[0][0], self.yticks[0][-1]) self.ylimrange_data = np.max(self.yticks[0]) - np.min(self.yticks[0]) self.ylims_plot = (0, 1) self.ylimrange_plot = 1.0 self.panel_drawn = False self.dataseries = [] #init list of dataseries to plot (List[Dict[str,Any]]) return
def __repr__(self) -> str: """returns string representation of the class""" return f"{self.__class__.__name__}(" + ", ".join([f"{attr}={val}" for attr, val in self.__dict__.items()]) + ")" #panel methods
[docs] def get_thetabounds(self) -> Tuple[float,float,float]: """returns panel location and bounds as angles in radians - method to compute bounds of the panel as an angle measured from the x-axis counterclockwise (in radians) Parameters Raises Returns - `theta_lb` - `float` - lower bound of the panel as an angle in radians - corresponds to `self.ylims_plot[0]` and `self.ylims_data[0]` - `theta_ub` - `float` - upper bound of the panel as an angle in radians - corresponds to `self.ylims_plot[1]` and `self.ylims_data[1]` - `theta_offset` - `float` - offset of the panel w.r.t. the x-axis - defines the angular position of the central ray of the panel - as an angle measured from the x-axis counterclockwise - in radians """ theta_offset = minmaxscale(self.theta, #panel position self.LSC.thetaplotlims[0], self.LSC.thetaplotlims[1], xmin_ref=self.LSC.thetaticks[0][0], xmax_ref=self.LSC.thetaticks[0][-1] ) theta_lb = theta_offset - self.panelsize/2 #lower bound of panel theta_ub = theta_offset + self.panelsize/2 #upper bound of panel return theta_offset, theta_lb, theta_ub
[docs] def get_rbounds(self) -> Tuple[float,float]: """returns panel bounds in radial direction - method to compute bounds of the panel in radial direction Parameters Raises Returns - `r_lb` - `float` - lower bound of the panel in x-direction (radially) - located at `self.xlimdeadzone*self.LSC.xlimrange_plot` - corresponds to `self.xlims[0]` - `r_ub` - `float` - upper bound of the panel in x-direction (radially) - located at `self.LSC.xlimrange_plot` - corresponds to `self.xlims[1]` Comments """ r_lb = self.LSC.xlimdeadzone*self.LSC.xlimrange_plot r_ub = self.LSC.xlimrange_plot return r_lb, r_ub
[docs] def get_yticks(self, theta_lb:float, theta_ub:float ) -> Tuple[List[float],List[Any]]: """returns yticklabels and location of yticks - method to compute angular positions of the y-ticks angles measured from the x-axis counterclockwise (in radians) Parameters - `theta_lb` - `float` - lower bound of the panel as an angle in radians - corresponds to `self.ylims_plot[0]` - `theta_ub` - `float` - upper bound of the panel as an angle in radians - corresponds to `self.ylims_plot[1]` Raises Returns - `ytickpos_th` - `List[float]` - tickpositions angles measured from the x-axis counterclockwise (in radians) - `yticklabs` - `List[Any]` - labels assigned to each tick - same length as `ytickpos_th` """ ytickpos_th = minmaxscale(self.yticks[0], theta_lb, theta_ub, xmin_ref=self.ylims_data[0], xmax_ref=self.ylims_data[1]) #no use of min/max to allow inverted axis yticklabs = self.yticks[1] return ytickpos_th, yticklabs
#dataseries methods
[docs] def apply_axis_limits(self, x:np.ndarray, y:np.ndarray, **kwargs, ) -> Tuple[np.ndarray,np.ndarray,Dict]: """returns `x`, `y` and `**kwargs` after application of axis limits - method enforce axis limits onto the dataseries - only applies out-of-bounds cuts - removes any datapoints that are out of bounds in x- or y-direction Parameters - `x` - `np.ndarray` - x-values of the series to be plotted - will serve as reference for enforcing `self.LSC.xlims_plot` - `y` - `np.ndarray` - y-values of the series to be plotted - will serve as reference for enforcing `self.ylims_data` - `**kwargs` - `kwargs` ultimately used when plotting `y` vs `x` - also get modified accordingly i.e., - `"c"` needs to be set to same size as `x_cut` and `y_cut` - `"s"` needs to be set to same size as `x_cut` and `y_cut` - `"alpha"` needs to be set to same size as `x_cut` and `y_cut` Raises Returns - `x_cut` - `np.ndarray` - `x` after applying axis-limit cuts - `y_cut` - `np.ndarray` - `y` after applying axis-limit cuts - `kwargs` - `Dict` - `**kwargs` after applying axis-limit cuts """ x_bool = (np.min(self.LSC.xlims_data)<=x)&(x<=np.max(self.LSC.xlims_data)) y_bool = (np.min(self.ylims_data)<=y)&(y<=np.max(self.ylims_data)) limitbool = (x_bool&y_bool) x_cut = x[limitbool] y_cut = y[limitbool] #modifications of array kwargs if "c" in kwargs.keys(): if isinstance(kwargs["c"], (np.ndarray, list)): kwargs["c"] = kwargs["c"][limitbool] if "s" in kwargs.keys(): if isinstance(kwargs["s"], (np.ndarray, list)) : kwargs["s"] = kwargs["s"][limitbool] if "alpha" in kwargs.keys(): if isinstance(kwargs["alpha"], (np.ndarray, list)) : kwargs["alpha"] = kwargs["alpha"][limitbool] return x_cut, y_cut, kwargs
#projection methods
[docs] def projection_preprocessing_(self, x:np.ndarray, y:np.ndarray, ) -> Tuple[np.ndarray,np.ndarray]: """returns `x` and `y` after applying transformations preceding all projection methods - method applying preprocessing to `x` and `y` - applies transformation that precede all projection methods - only called from within the projection methods Parameters - `x` - `np.ndarray` - x-values of the series to be projected into the panel - `y` - `np.ndarray` - y-values of the series to be projected into the panel Raises Returns - `x_prep` - `np.ndarray` - `x` after application of preprocessing - `y_prep` - `np.ndarray` - `y` after application of preprocessing """ #rescale to plotting range (0,1) for more consistent results x_01 = minmaxscale(x, self.LSC.xlims_plot[0], self.LSC.xlims_plot[1], #0, 1, xmin_ref=self.LSC.xlims_data[0], xmax_ref=self.LSC.xlims_data[1], #don't use min/max to allow for inverted axes ) y_01 = minmaxscale(y, self.ylims_plot[0], self.ylims_plot[1], #0, 1, xmin_ref=self.ylims_data[0], xmax_ref=self.ylims_data[1], #don't use min/max to allow for inverted axes ) #project x to obey axis-limits x_prep = minmaxscale(x_01, self.LSC.xlimdeadzone*self.LSC.xlimrange_plot, self.LSC.xlimrange_plot, xmin_ref=self.LSC.xlims_plot[0], xmax_ref=self.LSC.xlims_plot[1], ) #project y to fit into panel/obey axis limits y_scaler = minmaxscale(x_prep, #essentially scales x to [xlimdeadzone,1] #only needed if `self.xlims_plot[1] != 1` self.LSC.xlimdeadzone, 1, xmin_ref=self.LSC.xlimdeadzone*self.LSC.xlimrange_plot, xmax_ref=self.LSC.xlimrange_plot ) y_prep = y_scaler * y_01 return x_prep, y_prep
[docs] def project_xy_theta(self, x:np.ndarray, y:np.ndarray, ) -> Tuple[np.ndarray,np.ndarray]: """returns `x` and `y` after projection into the panel - method implementing a way to project `x` and `y` into the panel - operates in `theta`-space when projecting the series - advantages - more accurate representation of y-direction - downsides - more distorsion in x-direction Parameters - `x` - `np.ndarray` - x-values of the series to be projected into the panel - `y` - `np.ndarray` - y-values of the series to be projected into the panel Raises Returns - `x_proj` - `np.ndarray` - `x` after projection - `y_proj` - `np.ndarray` - `y` after projection """ #global variables theta_offset, theta_lb, theta_ub = self.get_thetabounds() #convert ylims_plot to theta-values r_min, th_min = cart2polar(self.LSC.xlims_plot[1], self.ylims_plot[0]) r_max, th_max = cart2polar(self.LSC.xlims_plot[1], self.ylims_plot[1]) logger.debug(f"{th_min=}, {th_max=}") #always np.pi+0 and np.pi+np.pi/4 since interval [0,1] chosen logger.debug(f"{self.LSC.xlims_plot=}, {self.ylims_plot=}") #preprocessing (applied to all projection methods) x_prep, y_prep = self.projection_preprocessing_(x, y) #convert to polar coords for transformations r, theta = cart2polar(x_prep, y_prep) ##rescale theta (i.e., make sure y obeys axis limits) theta = minmaxscale(theta, theta_lb, theta_ub, xmin_ref=th_min, xmax_ref=th_max, ) #convert back to cartesian coords for plotting #NOTE: use x_prep as radius because x is plotted in radial direction x_proj, y_proj = polar2cart(x_prep, theta) return x_proj, y_proj
[docs] def project_xy_y(self, x:np.ndarray, y:np.ndarray, ) -> Tuple[np.ndarray,np.ndarray, Dict]: """returns `x` and `y` after projection into the panel - method implementing a way to project `x` and `y` into the panel - operates in `y`-space when projecting the series - advantages - less distorsion in x-direction - downsides - can lead to unpredictable offsets in y-direction Parameters - `x` - `np.ndarray` - x-values of the series to be projected into the panel - `y` - `np.ndarray` - y-values of the series to be projected into the panel Raises Returns - `x_proj` - `np.ndarray` - `x` after projection - `y_proj` - `np.ndarray` - `y` after projection """ #global variables theta_offset, theta_lb, theta_ub = self.get_thetabounds() #preprocessing (applied to all projection methods) x_prep, y_prep = self.projection_preprocessing_(x, y) #project y to obey panel bounds x_slice = x_prep #x-coordinate of slice at every datapoint y_slice = x_slice * np.tan(self.panelsize) #y-coordinate of slice at every datapoint considering panel size y_slice_ub_ylim = self.LSC.xlims_plot[1] * np.tan(self.panelsize) #upper bound of the panel as defined by the y-limits y_slice_ub = max(np.max(y_slice), y_slice_ub_ylim) #actual upper bound of the panel (also considers dataseries being out of bounds) y_prep = y_slice_ub * y_prep - y_slice/2 #adjust projection of datapoints in y #offset by half of the datapoints y_slice to make avoid overflows #convert to polar coords for transformations r, theta = cart2polar(x_prep, y_prep) theta += theta_offset + np.pi #convert back to cartesian coords for plotting x_proj, y_proj = polar2cart(r, theta) return x_proj, y_proj
[docs] def project_xy(self, x:np.ndarray, y:np.ndarray, y_projection_method:Literal["theta","y"]="theta" ) -> Tuple[np.ndarray,np.ndarray]: """returns `x` and `y` after projection into the panel - method to project `x` and `y` into the panel using `y_projection_method` - calls upon `project_xy_...()` based on `y_projection_method` - generally `y_projection_method="theta"` is the preferred modus operandi Parameters - `x` - `np.ndarray` - x-values of the series to be projected into the panel - `y` - `np.ndarray` - y-values of the series to be projected into the panel - `y_projection_method` - `Literal["theta","y"]`, optioal - method to use for the projection - the default is `theta` - uses `self.project_xy_theta()` Raises Returns - `x_proj` - `np.ndarray` - `x` after projection - `y_proj` - `np.ndarray` - `y` after projection """ if y_projection_method == "theta": x_proj, y_proj = self.project_xy_theta(x, y) elif y_projection_method == "y": x_proj, y_proj = self.project_xy_y(x, y) else: raise ValueError(f"`y_projection_method` has to be one of `'theta'`, `'y'` but got {y_projection_method}") return x_proj, y_proj
#plotting methods
[docs] def plot(self, x:np.ndarray, y:np.ndarray, seriestype:Literal["line","scatter"]="line", **kwargs, ): """attaches a dataseries to plot to the panel - method to add a series to the panel for plotting Parameters - `x` - `np.ndarray` - x-values of the series - has to have same length as `y` - `y` - `np.ndarray` - y-values of the series - has to have same length as `x` - `seriestpye` - `Literal["line","scatter"]`, optional - which style to use for plotting the series `"line"` -- line plot `"scatter"` -- scatter plot - the default is `"line"` -`**kwargs` - kwargs to pass to `ax.plot()` Raises Returns """ #apply axis limits x_cut, y_cut, kwargs = self.apply_axis_limits(x, y, **kwargs) #project x and y x_proj, y_proj = self.project_xy(x_cut, y_cut, self.y_projection_method) self.dataseries.append(dict( x=x_proj, y=y_proj, x_cut=x_cut, y_cut=y_cut, seriestype=seriestype, kwargs=kwargs, )) return