Coverage for tcvx21/plotting/labels_m.py: 87%
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1"""
2Routines for adding labels to plots
3"""
4import matplotlib.pyplot as plt
5import numpy as np
6from matplotlib.offsetbox import AnchoredText
8from tcvx21.units_m import Quantity
9from tcvx21.observable_c.observable_m import Observable
11# Locations for legend or anchored text
12locations = {'best': 0, 'upper right': 1, 'upper left': 2, 'lower left': 3,
13 'lower right': 4, 'right': 5, 'center left': 6, 'center right': 7,
14 'lower center': 8, 'upper center': 9, 'center': 10}
17def add_y_zero_line(ax, level=0.0):
18 """Mark the 0-line"""
19 ax.axhline(level, color='k', linestyle='-', linewidth=0.5)
22def add_x_zero_line(ax):
23 """Mark the separatrix"""
24 ax.axvline(0.0, color='k', linestyle='-', linewidth=0.5)
27def add_twinx_label(ax, text: str, labelpad=20, visible=True):
28 """Adds a right-hand-side label with the field direction"""
29 new_ax = ax.twinx()
30 new_ax.set_yticks([])
31 new_ax.set_ylabel(text, rotation=270, labelpad=labelpad)
33 if not visible:
34 new_ax.set_xticks([])
35 new_ax.spines['top'].set_visible(False)
36 new_ax.spines['right'].set_visible(False)
37 new_ax.spines['bottom'].set_visible(False)
38 new_ax.spines['left'].set_visible(False)
40 # Switch focus back to the main axis
41 plt.sca(ax)
44def format_yaxis(ax):
45 """
46 Move the exponent into the ylabel
47 You have to call plt.draw() before this, otherwise the offset will be blank
48 """
49 plt.draw()
50 ax.yaxis.offsetText.set_visible(False)
51 units_string = f"{Quantity(1, ax.yaxis.units).units:~P}" if ax.yaxis.units else "Unitless"
53 units_string = units_string.lstrip('1')
55 # Get the axis scale factor (i.e. 10^19 for the density)
56 scale_factor = ax.get_yaxis().get_offset_text().get_text()
58 label_string = f"$10^{{{scale_factor.split('e')[-1]}}}${units_string}" if scale_factor \
59 else f"{units_string}"
61 ax.set_ylabel(label_string)
64def make_colorbar(ax, mappable, units, as_title: bool = True,
65 remove_offset: bool = True):
66 """
67 Adds a colorbar to the image, with a units label
68 """
69 cbar = plt.colorbar(mappable, ax=ax)
70 cax = cbar.ax
72 plt.draw()
73 units_string = f"{Quantity(1, units).units:~P}" if units else ""
75 if remove_offset:
76 # If the units string starts with a '1' (i.e. 1/m^3), remove the 1
77 units_string = units_string.lstrip('1')
78 cax.yaxis.offsetText.set_visible(False)
80 # Get the axis scale factor (i.e. 10^19 for the density)
81 scale_factor = cax.get_yaxis().get_offset_text().get_text()
83 label_string = f"[$10^{{{scale_factor.split('e')[-1]}}}${units_string}]" if scale_factor \
84 else f"[{units_string}]"
85 else:
86 label_string = f"[{units_string}]"
88 if as_title:
89 cax.set_title(label_string, fontsize=plt.rcParams['ytick.labelsize'])
90 else:
91 cax.set_ylabel(label_string, rotation=270, labelpad=15)
93 return cbar
96def make_field_direction_string(field_direction: str):
97 """Returns the field direction as a nice string"""
98 field_direction_rename = {
99 'forward_field': 'Forward field',
100 'reversed_field': 'Reversed field',
101 }
102 return field_direction_rename[field_direction]
105def make_diagnostic_string(diagnostic: str):
106 """Returns the diagnostic as a nice string"""
107 diagnostic_rename = {
108 'LFS-LP': 'Low-field-side target',
109 'LFS-IR': 'Low-field-side target',
110 'HFS-LP': 'High-field-side target',
111 'TS': 'Divertor Thomson',
112 'RDPA': 'Divertor volume',
113 'FHRP': 'Outboard midplane'
114 }
115 return diagnostic_rename[diagnostic]
118def make_observable_string(observable: Observable):
119 """Returns the observable as a compact string"""
120 observable_string = observable.name
122 shorten_title_rename = {
123 'Standard deviation': 'Std. dev.',
124 'of the': 'of',
125 'Pearson kurtosis': 'Kurtosis',
126 'Plasma velocity normalised to local sound speed': 'Mach number',
127 'Ion saturation current': '$J_{sat}$',
128 'ion saturation current': '$J_{sat}$'
129 }
130 for key, value in shorten_title_rename.items():
131 observable_string = observable_string.replace(key, value)
133 return observable_string
136def make_labels(field_direction: str, diagnostic: str, observable: Observable):
137 """
138 Returns strings which can be used in a title
139 """
141 return make_field_direction_string(field_direction), \
142 make_diagnostic_string(diagnostic), \
143 make_observable_string(observable)
146def label_subplots(axs: np.ndarray, prefix: str = None):
147 """Add subplot labels to an array of subplots"""
148 plt.draw()
150 index = -1
152 for ax in axs.flatten():
154 if not ax.get_visible():
155 continue
156 index += 1
158 # Convert integer to character
159 if prefix is None:
160 label = f"{chr(index + 65)}"
161 else:
162 label = f"{prefix}{chr(index + 65)}"
164 annotation_kwargs = dict(xycoords="axes fraction",
165 bbox=dict(
166 boxstyle="round",
167 alpha=0.9,
168 facecolor="white",
169 edgecolor="grey"
170 ))
172 annotate = ax.annotate(label, xy=(0.05, 0.85), **annotation_kwargs)
174 # Try to not overlap with the legend
175 legend = ax.get_legend()
176 try:
177 legend_extent = legend.get_window_extent()
178 except AttributeError:
179 continue
180 annotate_extent = annotate.get_window_extent()
182 if (((legend_extent.x0 <= annotate_extent.x0 <= legend_extent.x1) or
183 (legend_extent.x0 <= annotate_extent.x1 <= legend_extent.x1)) and
184 ((legend_extent.y0 <= annotate_extent.y0 <= legend_extent.y1) or
185 (legend_extent.y0 <= annotate_extent.y1 <= legend_extent.y1))):
186 # Prevent annotation from overlapping with legend
188 annotate.remove()
189 ax.annotate(label, xy=(0.05, 0.1), **annotation_kwargs)