mgplot.line_plot
line_plot.py: Plot a series or a dataframe with lines.
1""" 2line_plot.py: 3Plot a series or a dataframe with lines. 4""" 5 6# --- imports 7from typing import Any 8import math 9from collections.abc import Sequence 10from matplotlib.pyplot import Axes 11from pandas import DataFrame, Series, Period 12 13from mgplot.settings import DataT, get_setting 14from mgplot.kw_type_checking import ( 15 report_kwargs, 16 validate_kwargs, 17 validate_expected, 18 ExpectedTypeDict, 19) 20from mgplot.utilities import ( 21 apply_defaults, 22 get_color_list, 23 get_axes, 24 constrain_data, 25 check_clean_timeseries, 26 default_rounding, 27) 28from mgplot.keyword_names import ( 29 AX, 30 DROPNA, 31 PLOT_FROM, 32 LABEL_SERIES, 33 STYLE, 34 DRAWSTYLE, 35 MARKER, 36 MARKERSIZE, 37 WIDTH, 38 COLOR, 39 ALPHA, 40 ANNOTATE, 41 ROUNDING, 42 FONTSIZE, 43 FONTNAME, 44 ROTATION, 45 ANNOTATE_COLOR, 46) 47 48# --- constants 49LINE_KW_TYPES: ExpectedTypeDict = { 50 AX: (Axes, type(None)), 51 STYLE: (str, Sequence, (str,)), 52 WIDTH: (float, int, Sequence, (float, int)), 53 COLOR: (str, Sequence, (str,)), # line color 54 ALPHA: (float, Sequence, (float,)), 55 DRAWSTYLE: (str, Sequence, (str,), type(None)), 56 MARKER: (str, Sequence, (str,), type(None)), 57 MARKERSIZE: (float, Sequence, (float,), int, type(None)), 58 DROPNA: (bool, Sequence, (bool,)), 59 ANNOTATE: (bool, Sequence, (bool,)), 60 ROUNDING: (Sequence, (bool, int), int, bool, type(None)), 61 FONTSIZE: (Sequence, (str, int, float), str, int, float), 62 FONTNAME: (str, Sequence, (str,)), 63 ROTATION: (int, float, Sequence, (int, float)), 64 ANNOTATE_COLOR: (str, Sequence, (str,), bool, Sequence, (bool,), type(None)), 65 PLOT_FROM: (int, Period, type(None)), 66 LABEL_SERIES: (bool, Sequence, (bool,), type(None)), 67} 68validate_expected(LINE_KW_TYPES, "line_plot") 69 70 71# --- functions 72def annotate_series( 73 series: Series, 74 axes: Axes, 75 **kwargs, # "fontsize", "rounding", 76) -> None: 77 """Annotate the right-hand end-point of a line-plotted series.""" 78 79 # --- check the series has a value to annotate 80 latest = series.dropna() 81 if series.empty: 82 return 83 x, y = latest.index[-1], latest.iloc[-1] 84 if y is None or math.isnan(y): 85 return 86 87 # --- extract fontsize - could be None, bool, int or str. 88 fontsize = kwargs.get(FONTSIZE, "small") 89 if fontsize is None or isinstance(fontsize, bool): 90 fontsize = "small" 91 fontname = kwargs.get(FONTNAME, "Helvetica") 92 rotation = kwargs.get(ROTATION, 0) 93 94 # --- add the annotation 95 color = kwargs["color"] 96 rounding = default_rounding(value=y, provided=kwargs.get(ROUNDING, None)) 97 r_string = f" {y:.{rounding}f}" if rounding > 0 else f" {int(y)}" 98 axes.text( 99 x=x, 100 y=y, 101 s=r_string, 102 ha="left", 103 va="center", 104 fontsize=fontsize, 105 font=fontname, 106 rotation=rotation, 107 color=color, 108 ) 109 110 111def _get_style_width_color_etc( 112 item_count, num_data_points, **kwargs 113) -> tuple[dict[str, list | tuple], dict[str, Any]]: 114 """ 115 Get the plot-line attributes arguemnts. 116 Returns a dictionary of lists of attributes for each line, and 117 a modified kwargs dictionary. 118 """ 119 120 data_point_thresh = 151 # switch from wide to narrow lines 121 line_defaults: dict[str, Any] = { 122 STYLE: "solid" if item_count < 4 else ["solid", "dashed", "dashdot", "dotted"], 123 WIDTH: ( 124 get_setting("line_normal") 125 if num_data_points > data_point_thresh 126 else get_setting("line_wide") 127 ), 128 COLOR: get_color_list(item_count), 129 ALPHA: 1.0, 130 DRAWSTYLE: None, 131 MARKER: None, 132 MARKERSIZE: 10, 133 DROPNA: True, 134 ANNOTATE: False, 135 ROUNDING: True, 136 FONTSIZE: "small", 137 FONTNAME: "Helvetica", 138 ROTATION: 0, 139 ANNOTATE_COLOR: True, 140 LABEL_SERIES: True, 141 } 142 143 return apply_defaults(item_count, line_defaults, kwargs) 144 145 146def line_plot(data: DataT, **kwargs) -> Axes: 147 """ 148 Build a single plot from the data passed in. 149 This can be a single- or multiple-line plot. 150 Return the axes object for the build. 151 152 Agruments: 153 - data: DataFrame | Series - data to plot 154 - kwargs: 155 /* chart wide arguments */ 156 - ax: Axes | None - axes to plot on (optional) 157 /* individual line arguments */ 158 - dropna: bool | list[bool] - whether to delete NAs frm the 159 data before plotting [optional] 160 - color: str | list[str] - line colors. 161 - width: float | list[float] - line widths [optional]. 162 - style: str | list[str] - line styles [optional]. 163 - alpha: float | list[float] - line transparencies [optional]. 164 - marker: str | list[str] - line markers [optional]. 165 - marker_size: float | list[float] - line marker sizes [optional]. 166 /* end of line annotation arguments */ 167 - annotate: bool | list[bool] - whether to annotate a series. 168 - rounding: int | bool | list[int | bool] - number of decimal places 169 to round an annotation. If True, a default between 0 and 2 is 170 used. 171 - fontsize: int | str | list[int | str] - font size for the 172 annotation. 173 - fontname: str - font name for the annotation. 174 - rotation: int | float | list[int | float] - rotation of the 175 annotation text. 176 - drawstyle: str | list[str] - matplotlib line draw styles. 177 - annotate_color: str | list[str] | bool | list[bool] - color 178 for the annotation text. If True, the same color as the line. 179 180 Returns: 181 - axes: Axes - the axes object for the plot 182 """ 183 184 # --- check the kwargs 185 me = "line_plot" 186 report_kwargs(called_from=me, **kwargs) 187 kwargs = validate_kwargs(LINE_KW_TYPES, me, **kwargs) 188 189 # --- check the data 190 data = check_clean_timeseries(data, me) 191 df = DataFrame(data) # we are only plotting DataFrames 192 df, kwargs = constrain_data(df, **kwargs) 193 194 # --- some special defaults 195 kwargs[LABEL_SERIES] = ( 196 kwargs.get(LABEL_SERIES, True) 197 if len(df.columns) > 1 198 else kwargs.get(LABEL_SERIES, False) 199 ) 200 201 # --- Let's plot 202 axes, kwargs = get_axes(**kwargs) # get the axes to plot on 203 if df.empty or df.isna().all().all(): 204 # Note: finalise plot should ignore an empty axes object 205 print(f"Warning: No data to plot in {me}().") 206 return axes 207 208 # --- get the arguments for each line we will plot ... 209 item_count = len(df.columns) 210 num_data_points = len(df) 211 swce, kwargs = _get_style_width_color_etc(item_count, num_data_points, **kwargs) 212 213 for i, column in enumerate(df.columns): 214 series = df[column] 215 series = series.dropna() if DROPNA in swce and swce[DROPNA][i] else series 216 if series.empty or series.isna().all(): 217 print(f"Warning: No data to plot for {column} in line_plot().") 218 continue 219 220 series.plot( 221 # Note: pandas will plot PeriodIndex against their ordinal values 222 ls=swce[STYLE][i], 223 lw=swce[WIDTH][i], 224 color=swce[COLOR][i], 225 alpha=swce[ALPHA][i], 226 marker=swce[MARKER][i], 227 ms=swce[MARKERSIZE][i], 228 drawstyle=swce[DRAWSTYLE][i], 229 label=( 230 column 231 if LABEL_SERIES in swce and swce[LABEL_SERIES][i] 232 else f"_{column}_" 233 ), 234 ax=axes, 235 ) 236 237 if swce[ANNOTATE][i] is None or not swce[ANNOTATE][i]: 238 continue 239 240 color = ( 241 swce[COLOR][i] 242 if swce[ANNOTATE_COLOR][i] is True 243 else swce[ANNOTATE_COLOR][i] 244 ) 245 annotate_series( 246 series, 247 axes, 248 color=color, 249 rounding=swce[ROUNDING][i], 250 fontsize=swce[FONTSIZE][i], 251 fontname=swce[FONTNAME][i], 252 rotation=swce[ROTATION][i], 253 ) 254 255 return axes
LINE_KW_TYPES: mgplot.kw_type_checking.ExpectedTypeDict =
{'ax': (<class 'matplotlib.axes._axes.Axes'>, <class 'NoneType'>), 'style': (<class 'str'>, <class 'collections.abc.Sequence'>, (<class 'str'>,)), 'width': (<class 'float'>, <class 'int'>, <class 'collections.abc.Sequence'>, (<class 'float'>, <class 'int'>)), 'color': (<class 'str'>, <class 'collections.abc.Sequence'>, (<class 'str'>,)), 'alpha': (<class 'float'>, <class 'collections.abc.Sequence'>, (<class 'float'>,)), 'drawstyle': (<class 'str'>, <class 'collections.abc.Sequence'>, (<class 'str'>,), <class 'NoneType'>), 'marker': (<class 'str'>, <class 'collections.abc.Sequence'>, (<class 'str'>,), <class 'NoneType'>), 'markersize': (<class 'float'>, <class 'collections.abc.Sequence'>, (<class 'float'>,), <class 'int'>, <class 'NoneType'>), 'dropna': (<class 'bool'>, <class 'collections.abc.Sequence'>, (<class 'bool'>,)), 'annotate': (<class 'bool'>, <class 'collections.abc.Sequence'>, (<class 'bool'>,)), 'rounding': (<class 'collections.abc.Sequence'>, (<class 'bool'>, <class 'int'>), <class 'int'>, <class 'bool'>, <class 'NoneType'>), 'fontsize': (<class 'collections.abc.Sequence'>, (<class 'str'>, <class 'int'>, <class 'float'>), <class 'str'>, <class 'int'>, <class 'float'>), 'fontname': (<class 'str'>, <class 'collections.abc.Sequence'>, (<class 'str'>,)), 'rotation': (<class 'int'>, <class 'float'>, <class 'collections.abc.Sequence'>, (<class 'int'>, <class 'float'>)), 'annotate_color': (<class 'str'>, <class 'collections.abc.Sequence'>, (<class 'str'>,), <class 'bool'>, <class 'collections.abc.Sequence'>, (<class 'bool'>,), <class 'NoneType'>), 'plot_from': (<class 'int'>, <class 'pandas._libs.tslibs.period.Period'>, <class 'NoneType'>), 'label_series': (<class 'bool'>, <class 'collections.abc.Sequence'>, (<class 'bool'>,), <class 'NoneType'>)}
def
annotate_series( series: pandas.core.series.Series, axes: matplotlib.axes._axes.Axes, **kwargs) -> None:
73def annotate_series( 74 series: Series, 75 axes: Axes, 76 **kwargs, # "fontsize", "rounding", 77) -> None: 78 """Annotate the right-hand end-point of a line-plotted series.""" 79 80 # --- check the series has a value to annotate 81 latest = series.dropna() 82 if series.empty: 83 return 84 x, y = latest.index[-1], latest.iloc[-1] 85 if y is None or math.isnan(y): 86 return 87 88 # --- extract fontsize - could be None, bool, int or str. 89 fontsize = kwargs.get(FONTSIZE, "small") 90 if fontsize is None or isinstance(fontsize, bool): 91 fontsize = "small" 92 fontname = kwargs.get(FONTNAME, "Helvetica") 93 rotation = kwargs.get(ROTATION, 0) 94 95 # --- add the annotation 96 color = kwargs["color"] 97 rounding = default_rounding(value=y, provided=kwargs.get(ROUNDING, None)) 98 r_string = f" {y:.{rounding}f}" if rounding > 0 else f" {int(y)}" 99 axes.text( 100 x=x, 101 y=y, 102 s=r_string, 103 ha="left", 104 va="center", 105 fontsize=fontsize, 106 font=fontname, 107 rotation=rotation, 108 color=color, 109 )
Annotate the right-hand end-point of a line-plotted series.
def
line_plot(data: ~DataT, **kwargs) -> matplotlib.axes._axes.Axes:
147def line_plot(data: DataT, **kwargs) -> Axes: 148 """ 149 Build a single plot from the data passed in. 150 This can be a single- or multiple-line plot. 151 Return the axes object for the build. 152 153 Agruments: 154 - data: DataFrame | Series - data to plot 155 - kwargs: 156 /* chart wide arguments */ 157 - ax: Axes | None - axes to plot on (optional) 158 /* individual line arguments */ 159 - dropna: bool | list[bool] - whether to delete NAs frm the 160 data before plotting [optional] 161 - color: str | list[str] - line colors. 162 - width: float | list[float] - line widths [optional]. 163 - style: str | list[str] - line styles [optional]. 164 - alpha: float | list[float] - line transparencies [optional]. 165 - marker: str | list[str] - line markers [optional]. 166 - marker_size: float | list[float] - line marker sizes [optional]. 167 /* end of line annotation arguments */ 168 - annotate: bool | list[bool] - whether to annotate a series. 169 - rounding: int | bool | list[int | bool] - number of decimal places 170 to round an annotation. If True, a default between 0 and 2 is 171 used. 172 - fontsize: int | str | list[int | str] - font size for the 173 annotation. 174 - fontname: str - font name for the annotation. 175 - rotation: int | float | list[int | float] - rotation of the 176 annotation text. 177 - drawstyle: str | list[str] - matplotlib line draw styles. 178 - annotate_color: str | list[str] | bool | list[bool] - color 179 for the annotation text. If True, the same color as the line. 180 181 Returns: 182 - axes: Axes - the axes object for the plot 183 """ 184 185 # --- check the kwargs 186 me = "line_plot" 187 report_kwargs(called_from=me, **kwargs) 188 kwargs = validate_kwargs(LINE_KW_TYPES, me, **kwargs) 189 190 # --- check the data 191 data = check_clean_timeseries(data, me) 192 df = DataFrame(data) # we are only plotting DataFrames 193 df, kwargs = constrain_data(df, **kwargs) 194 195 # --- some special defaults 196 kwargs[LABEL_SERIES] = ( 197 kwargs.get(LABEL_SERIES, True) 198 if len(df.columns) > 1 199 else kwargs.get(LABEL_SERIES, False) 200 ) 201 202 # --- Let's plot 203 axes, kwargs = get_axes(**kwargs) # get the axes to plot on 204 if df.empty or df.isna().all().all(): 205 # Note: finalise plot should ignore an empty axes object 206 print(f"Warning: No data to plot in {me}().") 207 return axes 208 209 # --- get the arguments for each line we will plot ... 210 item_count = len(df.columns) 211 num_data_points = len(df) 212 swce, kwargs = _get_style_width_color_etc(item_count, num_data_points, **kwargs) 213 214 for i, column in enumerate(df.columns): 215 series = df[column] 216 series = series.dropna() if DROPNA in swce and swce[DROPNA][i] else series 217 if series.empty or series.isna().all(): 218 print(f"Warning: No data to plot for {column} in line_plot().") 219 continue 220 221 series.plot( 222 # Note: pandas will plot PeriodIndex against their ordinal values 223 ls=swce[STYLE][i], 224 lw=swce[WIDTH][i], 225 color=swce[COLOR][i], 226 alpha=swce[ALPHA][i], 227 marker=swce[MARKER][i], 228 ms=swce[MARKERSIZE][i], 229 drawstyle=swce[DRAWSTYLE][i], 230 label=( 231 column 232 if LABEL_SERIES in swce and swce[LABEL_SERIES][i] 233 else f"_{column}_" 234 ), 235 ax=axes, 236 ) 237 238 if swce[ANNOTATE][i] is None or not swce[ANNOTATE][i]: 239 continue 240 241 color = ( 242 swce[COLOR][i] 243 if swce[ANNOTATE_COLOR][i] is True 244 else swce[ANNOTATE_COLOR][i] 245 ) 246 annotate_series( 247 series, 248 axes, 249 color=color, 250 rounding=swce[ROUNDING][i], 251 fontsize=swce[FONTSIZE][i], 252 fontname=swce[FONTNAME][i], 253 rotation=swce[ROTATION][i], 254 ) 255 256 return axes
Build a single plot from the data passed in. This can be a single- or multiple-line plot. Return the axes object for the build.
Agruments:
- data: DataFrame | Series - data to plot
- kwargs:
/* chart wide arguments /
- ax: Axes | None - axes to plot on (optional) /
- dropna: bool | list[bool] - whether to delete NAs frm the data before plotting [optional]
- color: str | list[str] - line colors.
- width: float | list[float] - line widths [optional].
- style: str | list[str] - line styles [optional].
- alpha: float | list[float] - line transparencies [optional].
- marker: str | list[str] - line markers [optional].
- marker_size: float | list[float] - line marker sizes [optional]. / end of line annotation arguments */
- annotate: bool | list[bool] - whether to annotate a series.
- rounding: int | bool | list[int | bool] - number of decimal places to round an annotation. If True, a default between 0 and 2 is used.
- fontsize: int | str | list[int | str] - font size for the annotation.
- fontname: str - font name for the annotation.
- rotation: int | float | list[int | float] - rotation of the annotation text.
- drawstyle: str | list[str] - matplotlib line draw styles.
- annotate_color: str | list[str] | bool | list[bool] - color for the annotation text. If True, the same color as the line.
Returns:
- axes: Axes - the axes object for the plot