Source code for mtpy.imaging.mtplot_tools.plot_settings

# -*- coding: utf-8 -*-
"""
Created on Sun Sep 25 15:20:43 2022

@author: jpeacock
"""

from __future__ import annotations

from typing import TYPE_CHECKING

import matplotlib.colorbar as mcb
import matplotlib.colors as colors

# =============================================================================
# Imports
# =============================================================================
import numpy as np
from matplotlib import __version__ as matplotlib_version

import mtpy.imaging.mtcolors as mtcl

from . import MTArrows, MTEllipse

if TYPE_CHECKING:
    from matplotlib.axes import Axes


# =============================================================================
# ==============================================================================
# Plot settings
# ==============================================================================
[docs] class PlotSettings(MTArrows, MTEllipse): """ Comprehensive plot settings for MT data visualization. Combines arrow and ellipse settings with figure, marker, line, and colorbar properties for creating publication-quality MT plots. Parameters ---------- **kwargs : dict, optional Keyword arguments to set any class attributes. Valid keys correspond to the extensive list of attributes below. Attributes ---------- fig_num : int Figure number, by default 1 fig_dpi : int Figure DPI resolution, by default 150 fig_size : tuple | None Figure size (width, height) in inches, by default None show_plot : bool Whether to display plot, by default True font_size : int Base font size for plot text, by default 7 font_weight : str Font weight ('normal', 'bold', etc.), by default 'bold' marker_size : float Size of plot markers, by default 2.5 marker_lw : float Line width of marker edges, by default 0.75 marker_color : str Default marker color, by default 'b' marker : str Default marker style, by default 'v' lw : float Default line width, by default 1 plot_title : str | None Plot title text, by default None xy_ls, yx_ls, det_ls, skew_ls, strike_ls : str Line styles for different components, by default ':' xy_marker, yx_marker, det_marker, skew_marker : str Marker styles for different components strike_inv_marker, strike_pt_marker, strike_tip_marker : str Marker styles for strike indicators xy_color, yx_color, det_color, skew_color : tuple RGB colors for different components strike_inv_color, strike_pt_color, strike_tip_color : tuple RGB colors for strike indicators x_limits, y_limits, res_limits, phase_limits : tuple | None Axis limits for various plot types tipper_limits, strike_limits, skew_limits, pt_limits : tuple | None Additional axis limits plot_z : bool Whether to plot impedance, by default True plot_tipper : str Tipper plotting mode ('n', 'y', 'yri'), by default 'n' plot_pt : bool Whether to plot phase tensor, by default False plot_strike : bool Whether to plot strike, by default False plot_skew : bool Whether to plot skew, by default False text_size : int Text annotation size, by default 7 text_weight : str Text annotation weight, by default 'normal' text_color : str Text annotation color, by default 'k' text_ha : str Text horizontal alignment, by default 'center' text_va : str Text vertical alignment, by default 'baseline' text_angle : float Text rotation angle, by default 0 text_x_pad, text_y_pad : float Text padding in x/y directions, by default 0 text_rotation : float Additional text rotation, by default 0 subplot_left, subplot_right, subplot_bottom, subplot_top : float Subplot spacing parameters subplot_wspace, subplot_hspace : float | None Width/height spacing between subplots cb_orientation : str Colorbar orientation ('vertical' or 'horizontal'), by default 'vertical' cb_position : tuple | None Colorbar position [left, bottom, width, height], by default None """ def __init__(self, **kwargs) -> None: super().__init__(**kwargs) # figure properties: self.fig_num = 1 self.fig_dpi = 150 self.fig_size = None self.show_plot = True self.font_size = 7 self.font_weight = "bold" self.marker_size = 2.5 self.marker_lw = 0.75 self.marker_color = "b" self.marker = "v" self.lw = 1 self.plot_title = None # line styles: self.xy_ls = ":" self.yx_ls = ":" self.det_ls = ":" self.skew_ls = ":" self.strike_ls = ":" # marker styles: self.xy_marker = "s" self.yx_marker = "o" self.det_marker = "v" self.skew_marker = "d" self.strike_inv_marker = "v" self.strike_pt_marker = "^" self.strike_tip_marker = ">" # marker color styles: self.xy_color = (0.25, 0.35, 0.75) self.yx_color = (0.75, 0.25, 0.25) self.det_color = (0.25, 0.75, 0.25) self.skew_color = (0.85, 0.35, 0) self.strike_inv_color = (0.2, 0.2, 0.7) self.strike_pt_color = (0.7, 0.2, 0.2) self.strike_tip_color = (0.2, 0.7, 0.2) # marker face color styles: self.xy_mfc = (0.25, 0.35, 0.75) self.yx_mfc = (0.75, 0.25, 0.25) self.det_mfc = (0.25, 0.75, 0.25) self.skew_mfc = (0.85, 0.35, 0) self.strike_inv_mfc = (0.2, 0.2, 0.7) self.strike_pt_mfc = (0.7, 0.2, 0.2) self.strike_tip_mfc = (0.2, 0.7, 0.2) # plot limits self.x_limits = None self.y_limits = None self.res_limits = None self.phase_limits = None self.tipper_limits = None self.strike_limits = None self.skew_limits = None self.pt_limits = None # Show Plot self.show_plot = True self.plot_z = True self.plot_tipper = "n" self.plot_pt = False self.plot_strike = False self.plot_skew = False self.text_size = 7 self.text_weight = "normal" self.text_color = "k" self.text_ha = "center" self.text_va = "baseline" self.text_angle = 0 self.text_x_pad = 0 self.text_y_pad = 0 self.text_rotation = 0 self.subplot_left = 0.09 self.subplot_right = 0.9 self.subplot_bottom = 0.09 self.subplot_top = 0.98 self.subplot_wspace = None self.subplot_hspace = None self.cb_orientation = "vertical" self.cb_position = None # Set class property values from kwargs and pop them for key, value in kwargs.items(): setattr(self, key, value) self.cb_label_dict = { "phiminang": r"$\Phi_{min}$ (deg)", "phimin": r"$\Phi_{min}$ (deg)", "phimaxang": r"$\Phi_{max}$ (deg)", "phimax": r"$\Phi_{max}$ (deg)", "phidet": r"Det{$\Phi$} (deg)", "skew": r"Skew (deg)", "normalized_skew": r"Normalized Skew (deg)", "ellipticity": r"Ellipticity", "skew_seg": r"Skew (deg)", "normalized_skew_seg": r"Normalized Skew (deg)", "geometric_mean": r"$\sqrt{\Phi_{min} \cdot \Phi_{max}}$", "strike": r"Azimuth (deg)", "azimuth": r"Azimuth (deg)", } @property def period_label_dict(self) -> dict[int, str]: """ Get LaTeX-formatted period labels for log10 scale. Returns ------- dict[int, str] Dictionary mapping integer exponents to LaTeX strings in format $10^{exponent}$ for range -20 to 20 """ return dict([(ii, "$10^{" + str(ii) + "}$") for ii in range(-20, 21)])
[docs] def set_period_limits(self, period: np.ndarray) -> tuple[float, float]: """ Calculate period axis limits as powers of 10. Parameters ---------- period : np.ndarray Array of period values Returns ------- tuple[float, float] Minimum and maximum limits as powers of 10 (floor and ceil) """ return ( 10 ** (np.floor(np.log10(period.min()))), 10 ** (np.ceil(np.log10(period.max()))), )
def _estimate_resistivity_min(self, res_array: np.ndarray) -> float: """ Estimate minimum non-zero resistivity value. Parameters ---------- res_array : np.ndarray Array of resistivity values Returns ------- float Minimum non-zero resistivity value """ nz = np.nonzero(res_array) return np.nanmin(res_array[nz]) def _estimate_resistivity_max(self, res_array: np.ndarray) -> float: """ Estimate maximum non-zero resistivity value. Parameters ---------- res_array : np.ndarray Array of resistivity values Returns ------- float Maximum non-zero resistivity value """ nz = np.nonzero(res_array) return np.nanmax(res_array[nz]) def _compute_power_ten_min(self, value: float) -> float: """ Compute nearest power of 10 below value (floor). Parameters ---------- value : float Input value Returns ------- float 10 raised to floor of log10(value) """ return 10 ** (np.floor(np.log10(value))) def _compute_power_ten_max(self, value: float) -> float: """ Compute nearest power of 10 above value (ceil). Parameters ---------- value : float Input value Returns ------- float 10 raised to ceil of log10(value) """ return 10 ** (np.ceil(np.log10(value))) def _estimate_resistivity_limits_min(self, res_list: list[np.ndarray]) -> float: """ Estimate minimum resistivity limit from multiple arrays. Parameters ---------- res_list : list[np.ndarray] List of resistivity arrays Returns ------- float Minimum limit as power of 10 (floor) """ return self._compute_power_ten_min( min([self._estimate_resistivity_min(rr) for rr in res_list]) ) def _estimate_resistivity_limits_max(self, res_list: list[np.ndarray]) -> float: """ Estimate maximum resistivity limit from multiple arrays. Parameters ---------- res_list : list[np.ndarray] List of resistivity arrays Returns ------- float Maximum limit as power of 10 (ceil) """ return self._compute_power_ten_max( max([self._estimate_resistivity_max(rr) for rr in res_list]) )
[docs] def set_resistivity_limits( self, resistivity: np.ndarray, mode: str = "od", scale: str = "log" ) -> list[float]: """ Calculate appropriate resistivity axis limits. Parameters ---------- resistivity : np.ndarray Resistivity array with shape (n_periods, 2, 2) for full tensor or (n_periods,) for determinant mode : str, optional Plotting mode: - 'od': off-diagonal components (xy, yx) - 'd': diagonal components (xx, yy) - 'det' or 'det_only': determinant only - 'all': all components by default 'od' scale : str, optional Scale type ('log' or 'linear'), by default 'log' Returns ------- list[float] [min_limit, max_limit] as powers of 10 Defaults to [0.1, 10000] if calculation fails """ if mode in ["od"]: res_list = [resistivity[:, 0, 1], resistivity[:, 1, 0]] elif mode == "d": res_list = [resistivity[:, 0, 0], resistivity[:, 1, 1]] elif mode in ["det", "det_only"]: res_list = [resistivity] elif mode in ["all"]: res_list = [ resistivity[:, 0, 0], resistivity[:, 0, 1], resistivity[:, 1, 0], resistivity[:, 1, 1], ] try: limits = [ self._estimate_resistivity_limits_min(res_list), self._estimate_resistivity_limits_max(res_list), ] except (ValueError, TypeError): limits = [0.1, 10000] if scale == "log": if limits[0] == 0: limits[0] = 0.1 return limits
[docs] def set_phase_limits( self, phase: np.ndarray, mode: str = "od" ) -> tuple[float, float] | list[float]: """ Calculate appropriate phase axis limits. Parameters ---------- phase : np.ndarray Phase array with shape (n_periods, 2, 2) for full tensor or (n_periods,) for determinant mode : str, optional Plotting mode: - 'od': off-diagonal components (0-90 range with adjustments) - 'd': diagonal components (-180 to 180) - 'det' or 'det_only': determinant phase by default 'od' Returns ------- tuple[float, float] | list[float] [min_limit, max_limit] in degrees Defaults to [0, 90] for 'od' or [-180, 180] for others if calculation fails """ if mode in ["od"]: try: nz_xy = np.nonzero(phase[:, 0, 1]) nz_yx = np.nonzero(phase[:, 1, 0]) ph_min = min( [ np.nanmin(phase[nz_xy, 0, 1]), np.nanmin(phase[nz_yx, 1, 0] + 180), ] ) if ph_min > 0: ph_min = 0 else: ph_min = round(ph_min / 5) * 5 ph_max = max( [ np.nanmax(phase[nz_xy, 0, 1]), np.nanmax(phase[nz_yx, 1, 0] + 180), ] ) if ph_max < 91: ph_max = 89.9 else: ph_max = round(ph_max / 5) * 5 return (ph_min, ph_max) except (ValueError, TypeError): return [0, 90] elif mode == "d": return (-180, 180) elif mode in ["det", "det_only"]: try: phase_det = np.linalg.det(phase) nz = np.nonzero(phase_det) phase_limits = [ np.amin(phase_det[nz]), np.amax(phase_det[nz]), ] if phase_limits[0] < -180: phase_limits[0] = -180 if phase_limits[1] > 180: phase_limits[1] = 180 return phase_limits except (ValueError, TypeError): return [-180, 180]
@property def xy_error_bar_properties(self) -> dict[str, str | float]: """ Get matplotlib errorbar properties for xy component. Returns ------- dict[str, str | float] Dictionary of errorbar kwargs including marker style, size, colors, line styles, and cap properties """ return { "marker": self.xy_marker, "ms": self.marker_size, "mew": self.lw, "mec": self.xy_color, "color": self.xy_color, "ecolor": self.xy_color, "ls": self.xy_ls, "lw": self.lw, "capsize": self.marker_size, "capthick": self.lw, } @property def yx_error_bar_properties(self) -> dict[str, str | float]: """ Get matplotlib errorbar properties for yx component. Returns ------- dict[str, str | float] Dictionary of errorbar kwargs including marker style, size, colors, line styles, and cap properties """ return { "marker": self.yx_marker, "ms": self.marker_size, "mew": self.lw, "mec": self.yx_color, "color": self.yx_color, "ecolor": self.yx_color, "ls": self.yx_ls, "lw": self.lw, "capsize": self.marker_size, "capthick": self.lw, } @property def det_error_bar_properties(self) -> dict[str, str | float]: """ Get matplotlib errorbar properties for determinant component. Returns ------- dict[str, str | float] Dictionary of errorbar kwargs including marker style, size, colors, line styles, and cap properties """ return { "marker": self.det_marker, "ms": self.marker_size, "mew": self.lw, "mec": self.det_color, "color": self.det_color, "ecolor": self.det_color, "ls": self.det_ls, "lw": self.lw, "capsize": self.marker_size, "capthick": self.lw, } @property def font_dict(self) -> dict[str, int | str]: """ Get font properties dictionary for matplotlib. Adjusts key names based on matplotlib version for compatibility. Returns ------- dict[str, int | str] Font properties with 'size'/'fontsize' and 'weight'/'fontweight' depending on matplotlib version """ if int(matplotlib_version.split(".")[1]) < 9: return {"size": self.font_size + 2, "weight": self.font_weight} else: return { "fontsize": self.font_size + 2, "fontweight": self.font_weight, }
[docs] def make_pt_cb(self, ax: Axes) -> mcb.Colorbar: """ Create phase tensor colorbar. Generates colorbar for phase tensor plots with appropriate color mapping, normalization, and labeling based on ellipse settings. Parameters ---------- ax : Axes Matplotlib axes to attach colorbar to Returns ------- mcb.Colorbar Configured matplotlib colorbar object """ cmap = mtcl.cmapdict[self.ellipse_cmap] if "seg" in self.ellipse_cmap: # normalize the colors norms = colors.BoundaryNorm(self.ellipse_cmap_bounds, cmap.N) # make the colorbar cb = mcb.Colorbar( ax, cmap=cmap, norm=norms, orientation=self.cb_orientation, ticks=self.ellipse_cmap_bounds[1:-1], ) else: cb = mcb.Colorbar( ax, cmap=cmap, norm=colors.Normalize( vmin=self.ellipse_range[0], vmax=self.ellipse_range[1], ), orientation=self.cb_orientation, ) # label the color bar accordingly cb.set_label( self.cb_label_dict[self.ellipse_colorby], fontdict=self.font_dict, ) # place the label in the correct location if self.cb_orientation == "horizontal": cb.ax.xaxis.set_label_position("top") cb.ax.xaxis.set_label_coords(0.5, 1.3) elif self.cb_orientation == "vertical": cb.ax.yaxis.set_label_position("right") cb.ax.yaxis.set_label_coords(1.25, 0.5) cb.ax.yaxis.tick_left() cb.ax.tick_params(axis="y", direction="in") return cb
@property def arrow_real_properties(self) -> dict[str, str | float | bool]: """ Get matplotlib arrow properties for real tipper component. Returns ------- dict[str, str | float | bool] Dictionary of arrow kwargs including line width, colors, head dimensions, and length settings """ return { "lw": self.arrow_lw, "facecolor": self.arrow_color_real, "edgecolor": self.arrow_color_real, "head_width": self.arrow_head_width, "head_length": self.arrow_head_length, "length_includes_head": False, } @property def arrow_imag_properties(self) -> dict[str, str | float | bool]: """ Get matplotlib arrow properties for imaginary tipper component. Returns ------- dict[str, str | float | bool] Dictionary of arrow kwargs including line width, colors, head dimensions, and length settings """ return { "lw": self.arrow_lw, "facecolor": self.arrow_color_imag, "edgecolor": self.arrow_color_imag, "head_width": self.arrow_head_width, "head_length": self.arrow_head_length, "length_includes_head": False, } @property def text_dict(self) -> dict[str, int | str | float]: """ Get text annotation properties dictionary. Returns ------- dict[str, int | str | float] Dictionary of text properties including size, weight, rotation angle, and color """ return { "size": self.text_size, "weight": self.text_weight, "rotation": self.text_angle, "color": self.text_color, }