"""
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