Source code for sattoolbox.plots.cyclers

"""
This module contains predefined property cycles.
Additionally this module contains helper functions to create, combine and manipulate property cycles for plots.
"""
from enum import Enum
import warnings
import matplotlib.rcsetup
import numpy as np
import cycler

#Basic properties to cycle over
cycle_props = set(('color', 'c', 'linestyle', 'ls', 'linewidth', 'lw', 'marker'))
_aliases = {'c': 'color', 'lw': 'linewidth', 'ls': 'linestyle',}
# Get all properties that can be cycled over from matplotlib.
# In a try-except block to avoid breaking the code if the access
# to the properties change in future versions of matplotlib
try:
    cycle_props.update(matplotlib.rcsetup._prop_validators.keys()) #pylint: disable=protected-access
except AttributeError:
    warnings.warn("The access to cycable properties has changed in matplotlib."+
                  f"Only the basic properties (and their aliases) {cycle_props} are available.")
try:
    _aliases.update(matplotlib.rcsetup._prop_aliases) #pylint: disable=protected-access
    # Add the aliases to the cycle_props
    cycle_props.update(_aliases.keys())
    # cycle_props.update(matplotlib.rcsetup._prop_aliases.keys()) #pylint: disable=protected-access
except AttributeError:
    warnings.warn("The access to the aliases of cycable properties has changed in matplotlib."+
                  f"Only the basic properties (and their aliases) {cycle_props} are available.")

[docs] class Colors(Enum): """ An enumeration class `Colors` that defines various color palettes. New color-palettes can be added by creating a new enum member with a tuple of color codes. New Enum Members should avoid enum-names that are actually colors themselves. Attributes (Enum members): SAT (tuple): A color palette similar to Okabe and Ito but in a different order. Suitable for color-blind-friendly plots. Colors: blue, pink, green, orange, black, light blue, yellow, and gold. OKABEITO (tuple): The Okabe and Ito color palette, designed to be colorblind-friendly. Colors: black, green, blue, light blue, yellow, gold, orange, and pink. RGB (tuple): A basic RGB color palette with primary and secondary colors. Colors: red, green, blue, yellow, magenta, and cyan. """ SAT = ('#0071b2', '#cc79a7', '#009e74', '#d55e00', '#000000', '#56b3e9', '#f0e442', '#e69f00') OKABEITO = ('#000000', '#009e74', '#0071b2', '#56b3e9', '#f0e442', '#e69f00', '#d55e00', '#cc79a7') RGB = ('#FF0000', '#00FF00', '#0000FF', '#FFFF00', '#FF00FF', '#00FFFF')
[docs] class Linestyles(Enum): """ An enumeration class that defines various line styles for plotting. These styles can be used to customize the appearance of lines in plots. New Enum Members should avoid enum-names that are actually linestyles themselves. Attributes (Enum members): SAT (tuple): A collection of line styles, including predefined styles such as 'solid', 'dashed', 'dotted', and 'dashdot', as well as custom patterns defined by tuples. The custom patterns are specified as (offset, (on_off_sequence)), where 'offset' is the starting point of the pattern, and 'on_off_sequence' defines the lengths of dashes and spaces. Examples include: - 'loosely dotted': (0, (1, 10)) - 'densely dashdotdotted': (0, (3, 1, 1, 1, 1, 1)) These styles are inspired by the Matplotlib documentation: https://matplotlib.org/stable/gallery/lines_bars_and_markers/linestyles.html MAIN (tuple): A simplified collection of commonly used line styles, including: - 'solid' - 'dashed' - 'dotted' - 'dashdot' """ SAT = ('solid', # 'solid' 'dashed', # 'dashed' 'dotted', # 'dotted' 'dashdot', # 'dashdot' (0, (3, 5, 1, 5, 1, 5)), # 'dashdotdotted' (0, (1, 10)), # 'loosely dotted' (0, (5, 10)), # 'loosely dashed' (0, (3, 1, 1, 1)), # 'densely dashdotted' (0, (3, 10, 1, 10, 1, 10)), # 'loosely dashdotdotted' (0, (1, 1)), # 'densely dotted' (5, (10, 3)), # 'long dash with offset' (0, (5, 1)), # 'densely dashed' (0, (3, 10, 1, 10)), # 'loosely dashdotted' (0, (3, 1, 1, 1, 1, 1)) # 'densely dashdotdotted' ) # taken from https://matplotlib.org/stable/gallery/lines_bars_and_markers/linestyles.html MAIN = ('solid', 'dashed', 'dotted', 'dashdot')
[docs] class Markers(Enum): """ An enumeration representing different marker cyclers for plots. These markers can be used to customize the appearance of points in plots. New Enum Members should avoid enum-names that are actually markers themselves. Attributes (Enum members): SAT (tuple): A tuple of marker styles typically used for satellite-related plots. Includes markers such as '+', 'x', '2', '*', '|', '_', '1'. FILLED (tuple): A tuple of filled marker styles for plots. Includes markers such as 'o', 'X', 's', 'v', 'p', '<', 'h', '>'. OPEN (tuple): A tuple of open marker styles, similar to SAT. Includes markers such as '+', 'x', '2', '*', '|', '_', '1'. """ SAT = ('+', 'x', '$*$', '2', '|', '_', '1') FILLED = ('o','s', 'v', 'X', 'p', '<', 'h', '>') OPEN = ('+', 'x', '$*$', '2', '|', '_', '1')
[docs] def create_cycle(cycle_dict=None, n_lines=0, **kwargs): """ Creates a cycler object based on the provided cycle dictionary and additional properties. This function adjusts the cycle dictionary to account for the number of lines already plotted and optionally updates it with additional properties provided in `kwargs`. Parameters ---------- cycle_dict : dict, optional A dictionary representing the cycle properties. Keys are property names (e.g., 'color', 'linestyle'), and values are lists of values to cycle through. Defaults to an empty dictionary. This will usually be coming from plt.rcParams['axes.prop_cycle'].by_key() or from the cycler used on the last axes object when plotting multiple axes. n_lines : int, optional The number of lines already plotted. Used to adjust the cycle so that it continues from the correct position. Defaults to 0. **kwargs Additional properties to update the cycle dictionary. These properties will override existing ones in `cycle_dict`. Returns ------- tuple A tuple containing: - cycler.Cycler The resulting cycler object created from the modified cycle dictionary. This cycler can be used for plots. It is a 'least common multiple' of all passed properties. - dict The updated cycle dictionary after modifications. - dict Remaining keyword arguments that were not used in the cycle dictionary. """ if cycle_dict is None: new_cycle_dict = {} else: new_cycle_dict = cycle_dict.copy() # new_kwargs = kwargs.copy() # Creating a copy with all aliases replaced with the actual property names new_kwargs = {_aliases.get(key, key): value for key, value in kwargs.items()} for key, value in new_cycle_dict.items(): if value is None or isinstance(value, (str, int, float)): continue elif n_lines > len(value): # The cycle is shorter than the number of lines already plotted n = n_lines % len(value) else: n = n_lines # Take the first num_lines from the cycle and put them at the end of the cycle new_cycle_dict[key] = value[n:]+value[:n] for key in cycle_props: if key in new_kwargs: new_cycle_dict[key] = get_cycle_from_tuple(key, new_kwargs.pop(key)) cycler = combine_cycle_props(**new_cycle_dict) return cycler, new_cycle_dict, new_kwargs
[docs] def combine_cycle_props(method='lcm', **kwargs): """ Combine properties from a dictionary into a cycler object using a specified method. This function ensures that cyclers have the same length for all properties and replaces empty strings ('') with 'None' for line styles to support matplotlib cycler validation. Parameters ---------- method : str, optional The method to combine the cycles. Currently, only 'lcm' (least common multiple) is supported. Default is 'lcm'. **kwargs : dict Arbitrary keyword arguments representing cycler properties. Each property can be a single value (str, int, or float) or a list of values. If a single value is provided, it will be wrapped in a list. Invalid keyword arguments are raised by matplotlib's validators. Returns ------- matplotlib.cycler.Cycler A cycler object combining the provided properties based on the specified method. Notes ----- - If 'linestyle' is provided as an empty string (''), it will be replaced with 'None' to ensure compatibility with matplotlib. - If 'linestyle' is a list, all empty strings in the list will be replaced with 'None'. - The 'lcm' method calculates the least common multiple of the lengths of the provided property lists and expands each list to match the LCM length. Examples -------- >>> combine_cycle_props(color=['red', 'blue', 'green'], linestyle=['-', '--']) cycler({'color': ['red', 'blue', 'green', 'red', 'blue', 'green']}, 'linestyle': ['-', '--', '-', '--', '-', '--']) """ # Replacing the commonly used '' as 'no line' with 'None' # The '' does not work in a matplotlib cycler linestyle = kwargs.get('linestyle', 0) if linestyle == '': kwargs['linestyle'] = 'None' elif not isinstance(linestyle, (str, float, int)): kwargs['linestyle'] = [x if x != '' else 'None' for x in linestyle] # Wrap all kwargs that are passed as a single string/int/float in a list kwargs = {key: [value] if isinstance(value, (str, int, float)) else value for key, value in kwargs.items()} new_kwargs = {} if method == 'lcm': # Use least common mutliple to combine the cycles # Find lcm using numpy lcm = np.lcm.reduce(np.array([len(c) for c in kwargs.values()])) # Expand all values to the lcm new_kwargs = {key: value*(int(lcm/len(value))) for key, value in kwargs.items()} else: raise ValueError(f"Unknown method '{method}'. Only 'lcm' is supported.") # cycle = matplotlib.rcsetup.cycler(**new_kwargs) cycle = cycler.cycler(**new_kwargs) return cycle
[docs] def get_cycle_from_tuple(key, value): """ Retrieve a property value from predefined types based on the given key and value. Parameters ---------- key : str The property type key, such as 'color', 'linestyle', or 'marker'. value : str The property value to be matched or transformed. Returns ------- Any The corresponding property value from the predefined types if the key and value are valid. If the key and value pair is not recognized, the input value is returned as-is. Notes ----- - If the value is 'cycle', it is transformed to 'SAT' (as the default style). - Attempt to match the value (case-insensitive) to an enum name in the property type. - If the value does not match any enum name, the original value is returned. """ prop_type = {'color': Colors, 'c': Colors, 'linestyle': Linestyles, 'ls': Linestyles, 'marker': Markers}.get(key, None) if prop_type is None or not isinstance(value, str): if value in ['cycle']: raise ValueError(f"No predefined cycle exists for key: '{key}'."+ f"Try passing the cycle manually with {key}=[{key}1, {key}2, ...]") return value # #raise ValueError(f"Unknown property type {key} or value is not a string.") if value in ['cycle']: value = 'SAT' upper_value = value.upper() try: cycle = prop_type[upper_value] return cycle.value except KeyError: # if the value is not a valid enum name return value