change formatting of hovering value in interactive plot

This commit is contained in:
2025-05-13 15:05:34 +02:00
parent 042be2bad4
commit a5766ad618
2 changed files with 100 additions and 33 deletions

View File

@@ -58,9 +58,9 @@ from mpl_toolkits.axes_grid1.anchored_artists import AnchoredDirectionArrows, An
from scipy.ndimage import zoom as sc_zoom from scipy.ndimage import zoom as sc_zoom
try: try:
from .utils import PCconf, princ_angle, rot2D, sci_not from .utils import PCconf, princ_angle, rot2D, sci_not, cursor_data
except ImportError: except ImportError:
from utils import PCconf, princ_angle, rot2D, sci_not from utils import PCconf, princ_angle, rot2D, sci_not, cursor_data
def plot_obs(data_array, headers, rectangle=None, shifts=None, savename=None, plots_folder="", **kwargs): def plot_obs(data_array, headers, rectangle=None, shifts=None, savename=None, plots_folder="", **kwargs):
@@ -363,10 +363,10 @@ def polarization_map(
fig = plt.figure(figsize=(7 * ratiox, 7 * ratioy), layout="constrained") fig = plt.figure(figsize=(7 * ratiox, 7 * ratioy), layout="constrained")
if ax is None: if ax is None:
ax = fig.add_subplot(111, projection=wcs) ax = fig.add_subplot(111, projection=wcs)
ax.set(aspect="equal", fc="k") # , ylim=[-0.05 * stkI.shape[0], 1.05 * stkI.shape[0]]) ax.set(aspect="equal") # , fc="w", ylim=[-0.05 * stkI.shape[0], 1.05 * stkI.shape[0]])
# fig.subplots_adjust(hspace=0, wspace=0, left=0.102, right=1.02) # fig.subplots_adjust(hspace=0, wspace=0, left=0.102, right=1.02)
# ax.coords.grid(True, color='white', ls='dotted', alpha=0.5) ax.coords.grid(True, color="grey", ls="dotted", alpha=0.5)
ax.coords[0].set_axislabel("Right Ascension (J2000)") ax.coords[0].set_axislabel("Right Ascension (J2000)")
ax.coords[0].set_axislabel_position("t") ax.coords[0].set_axislabel_position("t")
ax.coords[0].set_ticklabel_position("t") ax.coords[0].set_ticklabel_position("t")
@@ -403,10 +403,10 @@ def polarization_map(
"gist_ncar", "gist_ncar",
"viridis", "viridis",
]: ]:
ax.set_facecolor("black") ax.set_facecolor(plt.get_cmap(kwargs["cmap"])(0.0))
font_color = "white" font_color = "white"
else: else:
ax.set_facecolor("white") ax.set_facecolor(plt.get_cmap(kwargs["cmap"])(0.0))
font_color = "black" font_color = "black"
if display.lower() in ["i", "intensity"]: if display.lower() in ["i", "intensity"]:
@@ -549,12 +549,15 @@ def polarization_map(
text_props={"ec": "k", "fc": font_color, "alpha": 1, "lw": 0.5}, text_props={"ec": "k", "fc": font_color, "alpha": 1, "lw": 0.5},
arrow_props={"ec": "k", "fc": font_color, "alpha": 1, "lw": 1}, arrow_props={"ec": "k", "fc": font_color, "alpha": 1, "lw": 1},
) )
ax.add_artist(px_sc)
ax.add_artist(north_dir)
if display.lower() in ["i", "s_i", "snri", "pf", "p", "pa", "s_p", "snrp", "confp"] and step_vec != 0: if display.lower() in ["i", "s_i", "snri", "pf", "p", "pa", "s_p", "snrp", "confp"] and step_vec != 0:
if scale_vec == -1: if scale_vec == -1:
poldata[np.isfinite(poldata)] = 1.0 / 2.0 poldata[np.isfinite(poldata)] = 1.0 / 2.0
step_vec = 1 step_vec = 1
scale_vec = 2.0 scale_vec = 2.0
if maskP.any():
X, Y = np.meshgrid(np.arange(stkI.shape[1]), np.arange(stkI.shape[0])) X, Y = np.meshgrid(np.arange(stkI.shape[1]), np.arange(stkI.shape[0]))
U, V = (poldata * np.cos(np.pi / 2.0 + pangdata * np.pi / 180.0), poldata * np.sin(np.pi / 2.0 + pangdata * np.pi / 180.0)) U, V = (poldata * np.cos(np.pi / 2.0 + pangdata * np.pi / 180.0), poldata * np.sin(np.pi / 2.0 + pangdata * np.pi / 180.0))
ax.quiver( ax.quiver(
@@ -578,10 +581,7 @@ def polarization_map(
pol_sc = AnchoredSizeBar( pol_sc = AnchoredSizeBar(
ax.transData, scale_vec, r"$P$= 100 %", 4, pad=0.25, sep=5, borderpad=0.25, frameon=False, size_vertical=0.005, color=font_color ax.transData, scale_vec, r"$P$= 100 %", 4, pad=0.25, sep=5, borderpad=0.25, frameon=False, size_vertical=0.005, color=font_color
) )
ax.add_artist(pol_sc) ax.add_artist(pol_sc)
ax.add_artist(px_sc)
ax.add_artist(north_dir)
ax.annotate( ax.annotate(
r"$F_{{\lambda}}^{{int}}$({0:.0f} $\AA$) = {1} $ergs \cdot cm^{{-2}} \cdot s^{{-1}} \cdot \AA^{{-1}}$".format( r"$F_{{\lambda}}^{{int}}$({0:.0f} $\AA$) = {1} $ergs \cdot cm^{{-2}} \cdot s^{{-1}} \cdot \AA^{{-1}}$".format(
@@ -3057,12 +3057,15 @@ class pol_map(object):
def display(self, fig=None, ax=None, flux_lim=None): def display(self, fig=None, ax=None, flux_lim=None):
kwargs = dict([]) kwargs = dict([])
self.data, self.error, self.fmt = None, None, None
if self.display_selection is None: if self.display_selection is None:
self.display_selection = "total_flux" self.display_selection = "total_flux"
if flux_lim is None: if flux_lim is None:
flux_lim = self.flux_lim flux_lim = self.flux_lim
if self.display_selection.lower() in ["total_flux"]: if self.display_selection.lower() in ["total_flux"]:
self.data = self.I * self.map_convert self.data = self.I * self.map_convert
self.error = self.I_ERR * self.map_convert
self.fmt = "{0:.2e} ± {1:.2e}"
if flux_lim is None: if flux_lim is None:
vmin, vmax = (1.0 / 2.0 * np.median(self.data[self.data > 0.0]), np.max(self.data[self.data > 0.0])) vmin, vmax = (1.0 / 2.0 * np.median(self.data[self.data > 0.0]), np.max(self.data[self.data > 0.0]))
else: else:
@@ -3071,6 +3074,8 @@ class pol_map(object):
label = r"$F_{\lambda}$ [$ergs \cdot cm^{-2} \cdot s^{-1} \cdot \AA^{-1}$]" label = r"$F_{\lambda}$ [$ergs \cdot cm^{-2} \cdot s^{-1} \cdot \AA^{-1}$]"
elif self.display_selection.lower() in ["pol_flux"]: elif self.display_selection.lower() in ["pol_flux"]:
self.data = self.I * self.map_convert * self.P self.data = self.I * self.map_convert * self.P
self.error = self.I_ERR * self.map_convert
self.fmt = "{0:.2e} ± {1:.2e}"
if flux_lim is None: if flux_lim is None:
vmin, vmax = (1.0 / 2.0 * np.median(self.I[self.I > 0.0] * self.map_convert), np.max(self.I[self.I > 0.0] * self.map_convert)) vmin, vmax = (1.0 / 2.0 * np.median(self.I[self.I > 0.0] * self.map_convert), np.max(self.I[self.I > 0.0] * self.map_convert))
else: else:
@@ -3079,11 +3084,21 @@ class pol_map(object):
label = r"$P \cdot F_{\lambda}$ [$ergs \cdot cm^{-2} \cdot s^{-1} \cdot \AA^{-1}$]" label = r"$P \cdot F_{\lambda}$ [$ergs \cdot cm^{-2} \cdot s^{-1} \cdot \AA^{-1}$]"
elif self.display_selection.lower() in ["pol_deg"]: elif self.display_selection.lower() in ["pol_deg"]:
self.data = self.P * 100.0 self.data = self.P * 100.0
<<<<<<< Updated upstream
kwargs["vmin"], kwargs["vmax"] = 0.0, np.max(self.data[self.P > self.P_ERR]) kwargs["vmin"], kwargs["vmax"] = 0.0, np.max(self.data[self.P > self.P_ERR])
||||||| constructed merge base
kwargs["vmin"], kwargs["vmax"] = 0.0, min(np.max(self.data[self.P > self.P_ERR]), 100.0)
=======
self.error = self.P_ERR * 100.0
self.fmt = "{0:.2f} ± {1:.2f} %"
kwargs["vmin"], kwargs["vmax"] = 0.0, min(np.max(self.data[self.P > self.P_ERR]), 100.0)
>>>>>>> Stashed changes
kwargs["alpha"] = 1.0 - 0.75 * (self.P < self.P_ERR) kwargs["alpha"] = 1.0 - 0.75 * (self.P < self.P_ERR)
label = r"$P$ [%]" label = r"$P$ [%]"
elif self.display_selection.lower() in ["pol_ang"]: elif self.display_selection.lower() in ["pol_ang"]:
self.data = princ_angle(self.PA) self.data = princ_angle(self.PA)
self.error = self.PA_ERR
self.fmt = "{0:.2f} ± {1:.2f} °"
kwargs["vmin"], kwargs["vmax"] = 0, 180.0 kwargs["vmin"], kwargs["vmax"] = 0, 180.0
kwargs["alpha"] = 1.0 - 0.75 * (self.P < self.P_ERR) kwargs["alpha"] = 1.0 - 0.75 * (self.P < self.P_ERR)
label = r"$\theta_{P}$ [°]" label = r"$\theta_{P}$ [°]"
@@ -3114,6 +3129,10 @@ class pol_map(object):
self.im = ax.imshow(self.data, aspect="equal", cmap="inferno", **kwargs) self.im = ax.imshow(self.data, aspect="equal", cmap="inferno", **kwargs)
plt.rcParams.update({"font.size": 14}) plt.rcParams.update({"font.size": 14})
self.cbar = fig.colorbar(self.im, ax=ax, aspect=50, shrink=0.75, pad=0.025, label=label) self.cbar = fig.colorbar(self.im, ax=ax, aspect=50, shrink=0.75, pad=0.025, label=label)
if self.error is not None:
my_cursor = cursor_data(self.im, error=self.error, fmt=self.fmt)
self.im.get_cursor_data = my_cursor.get
self.im.format_cursor_data = my_cursor.format
plt.rcParams.update({"font.size": 10}) plt.rcParams.update({"font.size": 10})
fig.canvas.draw_idle() fig.canvas.draw_idle()
return self.im return self.im
@@ -3123,6 +3142,8 @@ class pol_map(object):
ax.set_ylim(0, self.data.shape[0]) ax.set_ylim(0, self.data.shape[0])
plt.rcParams.update({"font.size": 14}) plt.rcParams.update({"font.size": 14})
fig.colorbar(im, ax=ax, aspect=50, shrink=0.75, pad=0.025, label=label) fig.colorbar(im, ax=ax, aspect=50, shrink=0.75, pad=0.025, label=label)
if format_cursor_data is not None:
self.im.format_cursor_data = format_cursor_data
plt.rcParams.update({"font.size": 10}) plt.rcParams.update({"font.size": 10})
fig.canvas.draw_idle() fig.canvas.draw_idle()
return im return im
@@ -3506,6 +3527,7 @@ if __name__ == "__main__":
savename="_".join([Stokes_UV[0].header["FILENAME"], args.type]), savename="_".join([Stokes_UV[0].header["FILENAME"], args.type]),
plots_folder=args.static_pdf, plots_folder=args.static_pdf,
display=args.type, display=args.type,
cmap="hot_r",
) )
else: else:
polarization_map( polarization_map(

View File

@@ -1,4 +1,5 @@
import numpy as np import numpy as np
from matplotlib.transforms import Bbox, BboxTransform
def rot2D(ang): def rot2D(ang):
@@ -154,6 +155,50 @@ def sci_not(v, err, rnd=1, out=str):
return *output[1:], -power return *output[1:], -power
class cursor_data:
"""
Object to overwrite data getter and formatter in interactive plots.
"""
def __init__(self, im, error=None, fmt=None) -> None:
self.im = im
self.data = im.get_array()
self.fmt = "{:.2f}" if fmt is None else fmt
self.err = error
def set_err(self, err) -> None:
if self.data.shape != err.shape:
raise ValueError("Error and Data don't have the same shape")
else:
self.err = err
def set_fmt(self, fmt) -> None:
self.fmt = fmt
def get(self, event):
xmin, xmax, ymin, ymax = self.im.get_extent()
if self.im.origin == "upper":
ymin, ymax = ymax, ymin
data_extent = Bbox([[xmin, ymin], [xmax, ymax]])
array_extent = Bbox([[0, 0], [self.data.shape[1], self.data.shape[0]]])
trans = self.im.get_transform().inverted()
trans += BboxTransform(boxin=data_extent, boxout=array_extent)
point = trans.transform([event.x, event.y])
if any(np.isnan(point)):
return None
j, i = point.astype(int)
# Clip the coordinates at array bounds
if not (0 <= i < self.data.shape[0]) or not (0 <= j < self.data.shape[1]):
return None
elif self.err is not None:
return self.data[i, j], self.err[i, j]
else:
return self.data
def format(self, y) -> str:
return self.fmt.format(*y)
def wcs_CD_to_PC(CD): def wcs_CD_to_PC(CD):
""" """
Return the position angle in degrees to the North direction of a wcs Return the position angle in degrees to the North direction of a wcs