'''
Matplotlib wrapper utilities for engineering plots.
This module provides reusable plotting infrastructure used by fluidsolve,
including figure/grid management, axis helpers, and optional interactive
widgets (sliders/buttons) for parameter exploration.
Scope:
* generic plotting primitives and containers,
* consistent figure sizing/layout setup,
* Tk-backed embedding and toolbar support,
* helper integration points for higher-level domain plots.
Design intent:
* centralize matplotlib boilerplate in one place,
* keep plot creation consistent across examples and tools,
* provide a stable base for domain modules such as ``plotext``.
Typical usage::
fig = PlotFigure(title='Q-H overview', nr=1, nc=1, toolbar=True)
graph = PlotGraph(fig, r=0, c=0)
curve = PlotCurve(graph, x=[0, 1, 2], y=[10, 8, 4])
fig.show()
By separating plotting infrastructure from hydraulic semantics, this module
keeps UI/figure concerns maintainable and reusable.
Reference inspiration:
https://medium.com/@basubinayak05/python-data-visualization-day-1-71334ff5044e
'''
# =============================================================================
# PYLINT DIRECTIVES
# =============================================================================
# =============================================================================
# IMPORTS
# =============================================================================
import os
from typing import Any, Callable
import weakref
import tkinter as tk
import numpy as np
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg, NavigationToolbar2Tk
import matplotlib.pyplot as plt
import matplotlib.gridspec as gridspec
from matplotlib.ticker import FormatStrFormatter, AutoMinorLocator
from matplotlib.widgets import Slider, Button
# module own
import fluidsolve.aux_tools as flsa
# =============================================================================
# HELPER FUNCTIONS
# =============================================================================
# =============================================================================
# PLOTFIGURE CLASS
# =============================================================================
# =============================================================================
# PLOTGRAPH CLASS
# =============================================================================
[docs]
class PlotGraph:
''' Matplotlib axes container.
Args:
figure (PlotFigure): Parent figure.
r (int | str): Row index or slice in the GridSpec.
c (int | str): Column index or slice in the GridSpec.
polar (bool, optional): Use polar projection.
title (str, optional): Axes title.
facecolor (str, optional): Axes background color.
edgecolor (str, optional): Axes edge color.
extra (dict, optional): Extra kwargs passed to add_subplot.
'''
[docs]
def __init__(self, figure: PlotFigure, **kwargs: int) -> None:
args = flsa.GetArgs(kwargs)
self._r: int | str = args.getArg(
'r',
[
flsa.vFun.istype(int, str, need=False),
flsa.vFun.tolambda(lambda x: (
x if isinstance(x, int) else
slice(None) if x == ':' else
slice(*map(int, x.split(':')))
), need=False),
]
)
self._c: int | str = args.getArg(
'c',
[
flsa.vFun.istype((int, str)),
flsa.vFun.tolambda(lambda x: (
x if isinstance(x, int) else
slice(None) if x == ':' else
slice(*map(int, x.split(':')))
), need=False),
]
)
self._polar: bool = args.getArg(
'polar',
[
flsa.vFun.default(None),
flsa.vFun.istype(bool, need=False),
]
)
self._title: str = args.getArg(
'title',
[
flsa.vFun.default(None),
flsa.vFun.istype(str, need=False),
]
)
self._facecolor: str = args.getArg(
'facecolor',
[
flsa.vFun.default(None),
flsa.vFun.istype(str, need=False),
]
)
self._edgecolor: str = args.getArg(
'edgecolor',
[
flsa.vFun.default(None),
flsa.vFun.istype(str, need=False),
]
)
self._extra: dict = {}
self._extra['main'] = args.getArg(
'extra',
[
flsa.vFun.default({}),
flsa.vFun.istype(dict, need=False),
]
)
#
self._parent : Any = weakref.ref(figure)
self._idx : int = figure.addGraph(self)
#
self._ax : Any = None
self._curves : Any = []
self._vlines : Any = []
self._hlines : Any = []
self._annotations : Any = []
self._xaxis : Any = None
self._xaxis2 : Any = None
self._yaxis : Any = None
self._yaxis2 : Any = None
self._grid : Any = None
self._legend : Any = None
@property
def axes(self) -> Any:
''' Underlying matplotlib Axes object. '''
return self._ax
[docs]
def getAxes(self, axis: str='main') -> Any:
''' Return the requested matplotlib Axes object. '''
if axis in ('main', 'x1', 'y1'):
return self._ax
if axis == 'x2':
return None if self._xaxis2 is None else self._xaxis2.axes
if axis == 'y2':
return None if self._yaxis2 is None else self._yaxis2.axes
raise ValueError(f'Invalid axis {axis}')
[docs]
def getAllAxes(self) -> list:
''' Return all configured matplotlib Axes objects without duplicates. '''
axes = [self._ax]
for axis in (self._xaxis2, self._yaxis2):
if axis is not None and axis.axes is not None and axis.axes not in axes:
axes.append(axis.axes)
return [ax for ax in axes if ax is not None]
[docs]
def setXAxis(self, **kwargs: Any) -> None:
''' Configure the primary X axis. '''
if not kwargs:
self._xaxis = None
else:
kwargs['type'] = 'x1'
self._xaxis = PlotAxis(self, **kwargs)
[docs]
def setYAxis(self, **kwargs: Any) -> Any:
''' Configure the primary Y axis. '''
if not kwargs:
self._yaxis = None
else:
kwargs['type'] = 'y1'
self._yaxis = PlotAxis(self, **kwargs)
[docs]
def setXAxis2(self, **kwargs: Any) -> Any:
''' Configure the secondary X axis. '''
if not kwargs:
self._xaxis2 = None
else:
kwargs['type'] = 'x2'
self._xaxis2 = PlotAxis(self, **kwargs)
[docs]
def setYAxis2(self, **kwargs: Any) -> Any:
''' Configure the secondary Y axis. '''
if not kwargs:
self._yaxis2 = None
else:
kwargs['type'] = 'y2'
self._yaxis2 = PlotAxis(self, **kwargs)
[docs]
def setGrid(self, **kwargs: Any) -> Any:
''' Configure the grid. '''
if not kwargs:
self._grid = None
else:
self._grid = PlotGrid(self, **kwargs)
[docs]
def setLegend(self, **kwargs: Any) -> None:
''' Configure the legend. '''
if not kwargs:
self._legend = None
else:
self._legend = PlotLegend(self, **kwargs)
[docs]
def addCurve(self, curve: 'PlotCurve') -> int:
''' Register a curve and return its index.
Args:
curve (PlotCurve): Curve to register.
Returns:
int: Index of the registered curve.
'''
self._curves.append(curve)
return len(self._curves) - 1
[docs]
def addVline(self, line: 'PlotLine') -> int:
''' Register a vertical line and return its index.
Args:
line (PlotLine): Line to register.
Returns:
int: Index of the registered line.
'''
self._vlines.append(line)
return len(self._vlines) - 1
[docs]
def addHline(self, line: 'PlotLine') -> int:
''' Register a horizontal line and return its index.
Args:
line (PlotLine): Line to register.
Returns:
int: Index of the registered line.
'''
self._hlines.append(line)
return len(self._hlines) - 1
[docs]
def addAnnotation(self, annotation: 'PlotAnnotation') -> int:
''' Register an annotation and return its index.
Args:
annotation (PlotAnnotation): Annotation to register.
Returns:
int: Index of the registered annotation.
'''
self._annotations.append(annotation)
return len(self._annotations) - 1
[docs]
def show(self) -> None:
''' Show the graph (axes); This is called by PlotFigure.show().
'''
#print('GRAPH show: ', self.__dict__)
fig = self._parent().figure
gs = self._parent().gridspec
args = flsa.prepareArgs(
polar = self._polar,
) | self._extra['main']
self._ax = fig.add_subplot(gs[self._r, self._c], **args)
if self._facecolor is not None:
self._ax.set_facecolor(self._facecolor)
if self._edgecolor is not None:
for spine in self._ax.spines.values():
spine.set_edgecolor(self._edgecolor)
if self._title:
titleargs = self._extra['title'] if 'title' in self._extra else {}
self._ax.set_title(self._title, **titleargs)
if self._xaxis is not None:
self._xaxis.show()
if self._yaxis is not None:
self._yaxis.show()
if self._xaxis2 is not None:
self._xaxis2.show()
if self._yaxis2 is not None:
self._yaxis2.show()
if self._grid is not None:
self._grid.show()
for c in self._curves:
c.show()
for l in self._vlines:
l.show()
for l in self._hlines:
l.show()
for a in self._annotations:
a.show()
if self._legend is not None:
self._legend.show()
#ax.title.set_text(self._title)
[docs]
def update(self) -> None:
''' Redraw curves and annotations, then re-apply axis limits and grid. '''
for c in self._curves:
c.update()
for l in self._vlines:
l.show()
for l in self._hlines:
l.show()
for a in self._annotations:
a.update()
if self._xaxis is not None:
self._xaxis.show()
if self._yaxis is not None:
self._yaxis.show()
if self._xaxis2 is not None:
self._xaxis2.show()
if self._yaxis2 is not None:
self._yaxis2.show()
if self._grid is not None:
self._grid.show()
if self._legend is not None:
self._legend.show()
[docs]
def updateData(self) -> None:
''' Redraw only curve and annotation data without touching axes or grid. '''
for c in self._curves:
c.updateData()
for a in self._annotations:
a.updateData()
# =============================================================================
# PLOTCURVE CLASS
# =============================================================================
[docs]
class PlotCurve:
''' A single data series on a PlotGraph.
Args:
graph (PlotGraph): Parent graph.
type (str, optional): Plot type; one of ``line``, ``scatter``, or ``bar``.
x (list, optional): X data.
y (list, optional): Y data.
label (str, optional): Legend label.
color (str, optional): Line or marker color.
alpha (float, optional): Opacity.
linestyle (str, optional): Line style.
marker (str, optional): Marker style.
extra (dict, optional): Extra kwargs passed to the plot call.
'''
[docs]
def __init__(self, graph: PlotGraph, **kwargs: int) -> None:
args : dict = flsa.GetArgs(kwargs)
self._type: str = args.getArg( # (line, scatter, bar, ....)
'type',
[
flsa.vFun.default('line'),
flsa.vFun.istype(str),
flsa.vFun.inlist('line', 'scatter', 'bar'),
]
)
self._x: list = args.getArg(
'x',
[
flsa.vFun.default([]),
flsa.vFun.istype(list),
]
)
self._y: list = args.getArg(
'y',
[
flsa.vFun.default([]),
flsa.vFun.istype(list),
]
)
self._label: str = args.getArg(
'label',
[
flsa.vFun.default(None),
flsa.vFun.istype(str, need=False),
]
)
self._color: str = args.getArg(
'color',
[
flsa.vFun.default(None),
flsa.vFun.istype(str, need=False),
]
)
self._alpha: float = args.getArg(
'alpha',
[
flsa.vFun.default(None),
flsa.vFun.istype(float, need=False),
]
)
self._linestyle: str = args.getArg(
'linestyle',
[
flsa.vFun.default(None),
flsa.vFun.istype(str, need=False),
]
)
self._marker: str = args.getArg(
'marker',
[
flsa.vFun.default(None),
flsa.vFun.istype(str, need=False),
]
)
self._axis: str = args.getArg(
'axis',
[
flsa.vFun.default('main'),
flsa.vFun.istype(str),
flsa.vFun.inlist('main', 'x1', 'y1', 'x2', 'y2'),
]
)
self._extra: dict = {}
self._extra['main'] = args.getArg(
'extra',
[
flsa.vFun.default({}),
flsa.vFun.istype(dict),
]
)
#
self._parent : Any = weakref.ref(graph)
self._idx : int = graph.addCurve(self)
#
self._curve : Any = None
@property
def x(self) -> list:
''' x data.
Returns:
list: x data.
'''
return self._x
@x.setter
def x(self, value:list) -> Any:
''' set x data.
Args:
value (list): x data.
'''
self._x = value
@property
def y(self) -> list:
''' y data.
Returns:
list: y data.
'''
return self._y
@y.setter
def y(self, value:list) -> Any:
''' set y data.
Args:
value (list): y data.
'''
self._y = value
@property
def curve(self) -> Any:
''' curve object.
Returns:
Any: curve object.
'''
return self._curve
[docs]
def show(self) -> None:
''' Show the data; This is called by PlotGraph.show().
'''
#print('CURVE show: ', self.__dict__)
ax = self._parent().getAxes(self._axis)
if ax is None:
raise ValueError(f'Axis {self._axis} is not configured on this graph')
args = flsa.prepareArgs(
label = self._label,
color = self._color,
alpha = self._alpha,
linestyle = self._linestyle,
marker = self._marker,
) | self._extra['main']
if self._type == 'line':
self._curve = ax.plot(self._x, self._y, **args)
elif self._type == 'scatter':
self._curve = ax.scatter(self._x, self._y, **args)
elif self._type == 'bar':
self._curve = ax.bar(self._x, height=self._y, **args)
[docs]
def update(self) -> None:
''' Redraw the curve with current data. '''
self.updateData()
[docs]
def updateData(self) -> None:
''' Update curve data in-place without rebuilding the artist. '''
#print('CURVE update: ', self.__dict__)
if self._type == 'line':
curve = self._curve[0]
curve.set_xdata(self._x)
curve.set_ydata(self._y)
elif self._type == 'scatter':
data = np.stack([self._x, self._y]).T
self._curve.set_offsets(data)
elif self._type == 'bar':
# Support both real matplotlib rectangles and simplified test doubles.
if len(self._curve) == 1 and hasattr(self._curve[0], 'set_ydata'):
self._curve[0].set_xdata(self._x)
self._curve[0].set_ydata(self._y)
else:
for rect, x_val, y_val in zip(self._curve, self._x, self._y):
rect.set_height(y_val)
rect.set_x(x_val - rect.get_width() / 2)
# =============================================================================
# PLOTLINE CLASS
# =============================================================================
[docs]
class PlotLine:
''' Horizontal or vertical reference line on a PlotGraph.
Args:
graph (PlotGraph): Parent graph.
typev (bool): True for vertical, False for horizontal.
v (float, optional): Position of the line.
x (float, optional): Relative start position (0-1).
y (float, optional): Relative end position (0-1).
label (str, optional): Legend label.
color (str, optional): Line color.
alpha (float, optional): Opacity.
linestyle (str, optional): Line style.
extra (dict, optional): Extra kwargs for the line call.
'''
[docs]
def __init__(self, graph: PlotGraph, **kwargs: int) -> None:
args : dict = flsa.GetArgs(kwargs)
self._typev: str = args.getArg( # (typev=true = vertical, false = horizontal)
'typev',
[
flsa.vFun.istype(bool),
]
)
self._v: float = args.getArg(
'v',
[
flsa.vFun.default(0.0),
flsa.vFun.istype(int, float, need=False),
flsa.vFun.totype(float, need=False),
]
)
self._min: float = args.getArg(
'x',
[
flsa.vFun.default(0.0),
flsa.vFun.istype(int, float, need=False),
flsa.vFun.totype(float, need=False),
]
)
self._max: float = args.getArg(
'y',
[
flsa.vFun.default(1.0),
flsa.vFun.istype(int, float, need=False),
flsa.vFun.totype(float, need=False),
]
)
self._label: str = args.getArg(
'label',
[
flsa.vFun.default(None),
flsa.vFun.istype(str, need=False),
]
)
self._color: str = args.getArg(
'color',
[
flsa.vFun.default(None),
flsa.vFun.istype(str, need=False),
]
)
self._alpha: float = args.getArg(
'alpha',
[
flsa.vFun.default(None),
flsa.vFun.istype(float, need=False),
]
)
self._linestyle: str = args.getArg(
'linestyle',
[
flsa.vFun.default(None),
flsa.vFun.istype(str, need=False),
]
)
self._extra: dict = {}
self._extra['main'] = args.getArg(
'extra',
[
flsa.vFun.default({}),
flsa.vFun.istype(dict),
]
)
#
self._parent : Any = weakref.ref(graph)
if self._typev:
self._idx : int = graph.addVline(self)
else:
self._idx : int = graph.addHline(self)
self._line : Any = None
#
[docs]
def show(self) -> None:
''' Show the line; This is called by PlotGraph.show().
'''
ax = self._parent().axes
if self._line is not None:
self._line.remove()
self._line = None
args = flsa.prepareArgs(
label = self._label,
color = self._color,
alpha = self._alpha,
linestyle = self._linestyle,
) | self._extra['main']
if self._typev:
self._line = ax.axvline(x=self._v, ymin=self._min, ymax=self._max, **args)
else:
self._line = ax.axhline(y=self._v, xmin=self._min, xmax=self._max, **args)
# =============================================================================
# PLOTAXIS CLASS
# =============================================================================
[docs]
class PlotAxis:
''' Configures axis properties on a PlotGraph.
Args:
graph (PlotGraph): Parent graph.
type (str): Axis type: ``x1``, ``y1``, ``x2``, or ``y2``. Set by PlotGraph.
share (object, optional): Axis to share scale with.
vmin (int | float, optional): Minimum axis value.
vmax (int | float, optional): Maximum axis value.
vstep (int | float, optional): Major tick step.
vmstep (int | float, optional): Minor tick step.
axison (bool, optional): Whether the axis is visible.
axiscolor (str, optional): Axis line color.
labeltxt (str, optional): Axis label text.
labelcolor (str, optional): Axis label color.
labelfmt (str, optional): Tick label format string.
extra (dict, optional): Extra kwargs for axis configuration.
'''
[docs]
def __init__(self, graph: PlotGraph, **kwargs: int) -> None:
args = flsa.GetArgs(kwargs)
self._type: str = args.getArg(
'type',
[
flsa.vFun.inlist('x1', 'y1', 'x2', 'y2'),
flsa.vFun.istype(str),
]
)
self._shared: str = args.getArg(
'share',
[
flsa.vFun.default(None),
flsa.vFun.istype(object, need=False),
]
)
self._vmin: int | float = args.getArg(
'vmin',
[
flsa.vFun.default(None),
flsa.vFun.istype(int, float, need=False),
]
)
self._vmax: int | float = args.getArg(
'vmax',
[
flsa.vFun.default(None),
flsa.vFun.istype(int, float, need=False),
]
)
self._vstep: int | float = args.getArg(
'vstep',
[
flsa.vFun.default(None),
flsa.vFun.istype(int, float, need=False),
]
)
self._vmstep: int | float = args.getArg( # minor step
'vmstep',
[
flsa.vFun.default(None),
flsa.vFun.istype(int, float, need=False),
]
)
self._axison: str = args.getArg(
'axison',
[
flsa.vFun.default(True),
flsa.vFun.istype(bool),
]
)
self._axiscolor: str = args.getArg(
'axiscolor',
[
flsa.vFun.default(None),
flsa.vFun.istype(str, need=False),
]
)
self._labeltxt: str = args.getArg(
'labeltxt',
[
flsa.vFun.default(None),
flsa.vFun.istype(str, need=False),
]
)
self._labelcolor: str = args.getArg(
'labelcolor',
[
flsa.vFun.default(None),
flsa.vFun.istype(str, need=False),
]
)
self._labelfmt: str = args.getArg(
'labelfmt',
[
flsa.vFun.default(None),
flsa.vFun.istype(str, need=False),
]
)
self._extra: dict = {}
self._extra['main'] = args.getArg(
'extra',
[
flsa.vFun.default({}),
flsa.vFun.istype(dict),
]
)
#
self._parent: Any = weakref.ref(graph)
self._ax2 : Any = None
@property
def axes(self) -> Any:
''' Return the matplotlib Axes configured for this axis object. '''
if self._type in ('x1', 'y1'):
return self._parent().axes
return self._ax2
[docs]
def show(self) -> dict:
''' Show the axis; This is called by PlotGraph.show().
'''
ax = self._parent().axes
has_manual_limits = self._vmin is not None or self._vmax is not None
has_manual_ticks = self._vstep is not None
if has_manual_ticks:
if self._vmin is None or self._vmax is None:
raise ValueError('Need vmin and vmax when vstep is provided.')
ticks = np.arange(self._vmin, self._vmax + 1, self._vstep)
args = {}
if self._type == 'x1':
if self._shared:
ax.sharex(self._shared.axes)
if has_manual_limits:
ax.set_xlim(left=self._vmin, right=self._vmax)
if has_manual_ticks:
ax.set_xticks(ticks, **args)
if self._vmstep is not None:
ax.xaxis.set_minor_locator(AutoMinorLocator(self._vmstep))
if self._labeltxt is not None:
labelargs = flsa.prepareArgs(color=self._labelcolor)
ax.set_xlabel(self._labeltxt, **labelargs)
if not self._axison:
ax.xaxis.set_visible(False)
if self._axiscolor is not None:
ax.tick_params(axis='x', colors=self._axiscolor)
if self._labelfmt is not None:
ax.xaxis.set_major_formatter(FormatStrFormatter(self._labelfmt))
elif self._type == 'y1':
if self._shared:
axshared = self._shared.axes
ax.sharey(axshared)
if has_manual_limits:
ax.set_ylim(bottom=self._vmin, top=self._vmax)
if has_manual_ticks:
ax.set_yticks(ticks, **args)
if self._vmstep is not None:
ax.yaxis.set_minor_locator(AutoMinorLocator(self._vmstep))
if self._labeltxt is not None:
labelargs = flsa.prepareArgs(color=self._labelcolor)
ax.set_ylabel(self._labeltxt, **labelargs)
if not self._axison:
ax.yaxis.set_visible(False)
if self._axiscolor is not None:
ax.tick_params(axis='y', colors=self._axiscolor)
if self._labelfmt is not None:
ax.yaxis.set_major_formatter(FormatStrFormatter(self._labelfmt))
elif self._type == 'x2':
if self._ax2 is None:
self._ax2 = ax.twiny()
ax2 = self._ax2
if has_manual_limits:
ax2.set_xlim(left=self._vmin, right=self._vmax)
if has_manual_ticks:
ax2.set_xticks(ticks, **args)
if self._vmstep is not None:
ax2.xaxis.set_minor_locator(AutoMinorLocator(self._vmstep))
if self._labeltxt is not None:
labelargs = flsa.prepareArgs(color=self._labelcolor)
ax2.set_xlabel(self._labeltxt, **labelargs)
if not self._axison:
ax2.xaxis.set_visible(False)
if self._axiscolor is not None:
ax2.tick_params(axis='x', colors=self._axiscolor)
if self._labelfmt is not None:
ax2.xaxis.set_major_formatter(FormatStrFormatter(self._labelfmt))
elif self._type == 'y2':
if self._ax2 is None:
self._ax2 = ax.twinx()
ax2 = self._ax2
if has_manual_limits:
ax2.set_ylim(bottom=self._vmin, top=self._vmax)
if has_manual_ticks:
ax2.set_yticks(ticks, **args)
if self._vmstep is not None:
ax2.yaxis.set_minor_locator(AutoMinorLocator(self._vmstep))
if self._labeltxt is not None:
labelargs = flsa.prepareArgs(color=self._labelcolor)
ax2.set_ylabel(self._labeltxt, **labelargs)
if not self._axison:
ax2.yaxis.set_visible(False)
if self._axiscolor is not None:
ax2.tick_params(axis='y', colors=self._axiscolor)
if self._labelfmt is not None:
ax2.yaxis.set_major_formatter(FormatStrFormatter(self._labelfmt))
# =============================================================================
# PLOTANNOTATION CLASS
# =============================================================================
[docs]
class PlotAnnotation:
''' Configures annotation properties on a PlotGraph.
Args:
graph (PlotGraph): Parent graph.
label (list, optional): Annotation text labels.
x (list, optional): X positions.
y (list, optional): Y positions.
textcoords (str, optional): Coordinate system for text offset.
xoffset (int | float, optional): Horizontal text offset.
yoffset (int | float, optional): Vertical text offset.
xtoggle (int | float, optional): Alternating horizontal shift.
ytoggle (int | float, optional): Alternating vertical shift.
fontsize (int, optional): Text font size.
color (str, optional): Text color.
bbox (dict, optional): Bounding box properties.
arrow (dict, optional): Arrow properties.
halignment (str, optional): Horizontal alignment.
valignment (str, optional): Vertical alignment.
extra (dict, optional): Extra kwargs passed to annotate.
'''
[docs]
def __init__(self, graph: PlotGraph, **kwargs: int) -> None:
args : dict = flsa.GetArgs(kwargs)
self._label: str = args.getArg(
'label',
[
flsa.vFun.default([]),
flsa.vFun.istype(list),
]
)
self._x: list = args.getArg(
'x',
[
flsa.vFun.default([]),
flsa.vFun.istype(list),
]
)
self._y: list = args.getArg(
'y',
[
flsa.vFun.default([]),
flsa.vFun.istype(list),
]
)
self._textcoords: str = args.getArg(
'textcoords',
[
flsa.vFun.default('offset points'),
flsa.vFun.inlist(('offset points', 'axes points', 'data')),
]
)
self._xoffset: int | float = args.getArg(
'xoffset',
[
flsa.vFun.default(0),
flsa.vFun.istype(int, float),
]
)
self._yoffset: int | float = args.getArg(
'yoffset',
[
flsa.vFun.default(0),
flsa.vFun.istype(int, float),
]
)
self._xtoggle: int | float = args.getArg(
'xtoggle',
[
flsa.vFun.default(0),
flsa.vFun.istype(int, float),
]
)
self._ytoggle: int | float = args.getArg(
'ytoggle',
[
flsa.vFun.default(0),
flsa.vFun.istype(int, float),
]
)
self._fontsize: int = args.getArg(
'fontsize',
[
flsa.vFun.default(None),
flsa.vFun.istype(int, need=False),
]
)
self._color: str = args.getArg(
'color',
[
flsa.vFun.default(None),
flsa.vFun.istype(str, need=False),
]
)
self._bbox: dict = args.getArg(
'bbox',
[
flsa.vFun.default(None),
flsa.vFun.istype(dict, need=False),
]
)
self._arrow: dict = args.getArg(
'arrow',
[
flsa.vFun.default(None),
flsa.vFun.istype(dict, need=False),
]
)
self._halignment: str = args.getArg(
'halignment',
[
flsa.vFun.default(None),
flsa.vFun.inlist('center', 'left', 'right', need=False),
]
)
self._valignment: str = args.getArg(
'valignment',
[
flsa.vFun.default(None),
flsa.vFun.inlist('center', 'top', 'bottom', 'baseline', need=False),
]
)
self._extra: dict = {}
self._extra['main'] = args.getArg(
'extra',
[
flsa.vFun.default({}),
flsa.vFun.istype(dict),
]
)
#
self._parent : Any = weakref.ref(graph)
self._idx : int = graph.addAnnotation(self)
#
self._annotations: list = []
@property
def x(self) -> list:
''' x data.
Returns:
list: x data.
'''
return self._x
@x.setter
def x(self, value:list) -> Any:
''' set x data.
Args:
value (list): x data.
'''
self._x = value
@property
def y(self) -> list:
''' y data.
Returns:
list: y data.
'''
return self._y
@y.setter
def y(self, value:list) -> Any:
''' set y data.
Args:
value (list): y data.
'''
self._y = value
@property
def label(self) -> list:
''' label data.
Returns:
list: label data.
'''
return self._label
@label.setter
def label(self, value:list) -> Any:
''' set label data.
Args:
value (list): label data.
'''
self._label = value
[docs]
def show(self) -> None:
''' Show the annotation; This is called by PlotGraph.show().
'''
if len(self._x) > 0 and len(self._y) > 0 and len(self._label) > 0:
if len(self._x) != len(self._y):
raise ValueError(f'PlotAnnotation: Size of x list: {len(self._x)} not equal to size of y list {len(self._y)}')
if len(self._x) != len(self._label):
raise ValueError(f'PlotAnnotation: Size of label list: {len(self._label)} not equal to size of x list {len(self._x)}')
ax: plt.axes = self._parent().axes
args = flsa.prepareArgs(
textcoords = self._textcoords,
bbox = self._bbox,
arrowprops = self._arrow,
fontsize = self._fontsize,
color = self._color,
ha = self._halignment,
va = self._valignment,
) | self._extra['main']
toggle = 1
for i, x_val in enumerate(self._x):
self._annotations.append(
ax.annotate(
self._label[i],
xy=(x_val, self._y[i]),
xytext=(self._xoffset + toggle * self._xtoggle, self._yoffset + toggle * self._ytoggle),
**args
)
)
toggle = -toggle
[docs]
def update(self) -> None:
''' Update the annotation by removing and redrawing it. '''
self.updateData()
[docs]
def updateData(self) -> None:
''' Remove and redraw all annotations with current data. '''
for a in self._annotations:
a.remove()
self._annotations = []
self.show()
# =============================================================================
# PLOTGRID CLASS
# =============================================================================
[docs]
class PlotGrid:
''' Configures grid properties on a PlotGraph.
Args:
graph (PlotGraph): Parent graph.
axis (str, optional): Which axis to apply the grid to: ``x``, ``y``, or ``both``.
color (str, optional): Grid line color.
linestyle (str, optional): Grid line style.
linewidth (int | float, optional): Grid line width.
extra (dict, optional): Extra kwargs passed to ax.grid.
'''
[docs]
def __init__(self, graph: PlotGraph, **kwargs: int) -> None:
args = flsa.GetArgs(kwargs)
self._axis: str = args.getArg(
'axis',
[
flsa.vFun.default('both'),
flsa.vFun.inlist('x', 'y', 'both'),
flsa.vFun.istype(str),
]
)
self._color: str = args.getArg(
'color',
[
flsa.vFun.default(None),
flsa.vFun.istype(str, need=False),
]
)
self._linestyle: str = args.getArg(
'linestyle',
[
flsa.vFun.default(None),
flsa.vFun.istype(str, need=False),
]
)
self._linewidth: int | float = args.getArg(
'linewidth',
[
flsa.vFun.default(None),
flsa.vFun.istype(int, float, need=False),
]
)
self._extra: dict = {}
self._extra['main'] = args.getArg(
'extra',
[
flsa.vFun.default({}),
flsa.vFun.istype(dict),
]
)
#
self._parent: Any = weakref.ref(graph)
[docs]
def show(self) -> dict:
''' Show the grid; This is called by PlotGraph.show().
'''
ax = self._parent().axes
args = flsa.prepareArgs(
axis = self._axis,
color = self._color,
linestyle = self._linestyle,
linewidth = self._linewidth,
) | self._extra['main']
ax.grid(**args)
# =============================================================================
# PLOTLEGEND CLASS
# =============================================================================
[docs]
class PlotLegend:
''' Configures a legend on a PlotGraph.
Args:
graph (PlotGraph): Parent graph.
loc (str, optional): Legend location (e.g. ``best``, ``upper right``). Default ``best``.
title (str, optional): Legend title.
fontsize (int, optional): Font size for legend entries.
title_fontsize (int, optional): Font size for the legend title.
frameon (bool, optional): Whether to draw the legend frame. Default True.
ncols (int, optional): Number of columns. Default 1.
extra (dict, optional): Extra kwargs passed to ax.legend.
'''
[docs]
def __init__(self, graph: PlotGraph, **kwargs: int) -> None:
args = flsa.GetArgs(kwargs)
self._loc: str = args.getArg(
'loc',
[
flsa.vFun.default('best'),
flsa.vFun.istype(str, need=False),
]
)
self._title: str = args.getArg(
'title',
[
flsa.vFun.default(None),
flsa.vFun.istype(str, need=False),
]
)
self._fontsize: int = args.getArg(
'fontsize',
[
flsa.vFun.default(None),
flsa.vFun.istype(int, need=False),
]
)
self._title_fontsize: int = args.getArg( # pylint: disable=invalid-name
'title_fontsize',
[
flsa.vFun.default(None),
flsa.vFun.istype(int, need=False),
]
)
self._frameon: bool = args.getArg(
'frameon',
[
flsa.vFun.default(True),
flsa.vFun.istype(bool),
]
)
self._ncols: int = args.getArg(
'ncols',
[
flsa.vFun.default(1),
flsa.vFun.istype(int),
]
)
self._extra: dict = {}
self._extra['main'] = args.getArg(
'extra',
[
flsa.vFun.default({}),
flsa.vFun.istype(dict),
]
)
self._parent: Any = weakref.ref(graph)
[docs]
def show(self) -> None:
''' Show the legend; This is called by PlotGraph.show(). '''
graph = self._parent()
ax = graph.axes
args = flsa.prepareArgs(
loc = self._loc,
title = self._title,
fontsize = self._fontsize,
title_fontsize = self._title_fontsize,
frameon = self._frameon,
ncols = self._ncols,
) | self._extra['main']
handles = []
labels = []
for axis in graph.getAllAxes():
axis_handles, axis_labels = axis.get_legend_handles_labels()
handles.extend(axis_handles)
labels.extend(axis_labels)
args = args | flsa.prepareArgs(handles=handles, labels=labels)
ax.legend(**args)
# =============================================================================
# PLOTBUTTON CLASS
# =============================================================================
#print('BUTTON show: ', self.__dict__)
# =============================================================================
# PLOTSLIDER CLASS
# =============================================================================
[docs]
class PlotSlider:
''' Matplotlib slider widget.
Args:
fig (PlotFigure): Parent figure.
r (int | str): Row index or slice in the widget GridSpec.
c (int | str): Column index or slice in the widget GridSpec.
label (str, optional): Slider label text.
color (str, optional): Slider color.
hovercolor (str, optional): Slider hover color.
vmin (int | float): Minimum slider value.
vmax (int | float): Maximum slider value.
vstep (int | float, optional): Slider step size.
vinit (int | float, optional): Initial slider value.
fun (Callable): Callback executed on slider change.
extra (dict, optional): Extra kwargs passed to the Slider constructor.
'''
[docs]
def __init__(self, fig: PlotFigure, **kwargs: int) -> None:
args = flsa.GetArgs(kwargs)
self._r: int | str = args.getArg(
'r',
[
flsa.vFun.istype(int, str),
flsa.vFun.tolambda(lambda x: (
x if isinstance(x, int) else
slice(None) if x == ':' else
slice(*map(int, x.split(':')))
)),
]
)
self._c: int | str = args.getArg(
'c',
[
flsa.vFun.istype(int, str),
flsa.vFun.tolambda(lambda x: (
x if isinstance(x, int) else
slice(None) if x == ':' else
slice(*map(int, x.split(':')))
)),
]
)
self._label: str = args.getArg(
'label',
[
flsa.vFun.default(' '),
flsa.vFun.istype(str),
]
)
self._color: str = args.getArg(
'color',
[
flsa.vFun.default(None),
flsa.vFun.istype(str, need=False),
]
)
self._hovercolor: str = args.getArg(
'hovercolor',
[
flsa.vFun.default(None),
flsa.vFun.istype(str, need=False),
]
)
self._vmin: int | float = args.getArg(
'vmin',
[
flsa.vFun.istype(int, float),
]
)
self._vmax: int | float = args.getArg(
'vmax',
[
flsa.vFun.istype(int, float)
]
)
self._vstep: int | float = args.getArg(
'vstep',
[
flsa.vFun.default(1),
flsa.vFun.istype(int, float),
]
)
self._vinit: int | float = args.getArg(
'vinit',
[
flsa.vFun.default(self._vmin),
flsa.vFun.istype(int, float),
]
)
self._fun: Callable = args.getArg(
'fun',
[
flsa.vFun.istype(Callable),
]
)
self._extra: dict = {}
self._extra['main'] = args.getArg(
'extra',
[
flsa.vFun.default({}),
flsa.vFun.istype(dict),
]
)
#
self._parent : Any = weakref.ref(fig)
self._idx : int = fig.addSlider(self)
self._ax : Any = None
self._widget : Any = None
@property
def widget(self) -> Any:
return self._widget
[docs]
def show(self) -> dict:
''' Show the slider; This is called by PlotFigure.show().
'''
fig = self._parent().figure_widgets
gs = self._parent().gridspec_widgets
self._ax = fig.add_subplot(gs[self._r, self._c])
# TODO need to do something with this?
#self._ax.set_zorder(10)
args = flsa.prepareArgs(
color = self._color,
hovercolor = self._hovercolor,
label = self._label,
valmin = self._vmin,
valmax = self._vmax,
valinit = self._vinit,
valstep = self._vstep,
)
self._widget = Slider(ax=self._ax, **args)
self._widget.on_changed(self._fun)