pylocuszoom 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,288 @@
1
+ """Matplotlib backend for pyLocusZoom.
2
+
3
+ Default backend providing static publication-quality plots.
4
+ """
5
+
6
+ from typing import Any, List, Optional, Tuple, Union
7
+
8
+ import matplotlib.pyplot as plt
9
+ import pandas as pd
10
+ from matplotlib.axes import Axes
11
+ from matplotlib.figure import Figure
12
+ from matplotlib.patches import Rectangle
13
+ from matplotlib.ticker import FuncFormatter, MaxNLocator
14
+
15
+
16
+ class MatplotlibBackend:
17
+ """Matplotlib backend for static plot generation.
18
+
19
+ This is the default backend, producing publication-quality static plots
20
+ suitable for papers and presentations.
21
+ """
22
+
23
+ def __init__(self) -> None:
24
+ """Initialize the matplotlib backend."""
25
+ pass
26
+
27
+ def create_figure(
28
+ self,
29
+ n_panels: int,
30
+ height_ratios: List[float],
31
+ figsize: Tuple[float, float],
32
+ sharex: bool = True,
33
+ ) -> Tuple[Figure, List[Axes]]:
34
+ """Create a figure with multiple panels.
35
+
36
+ Args:
37
+ n_panels: Number of vertical panels.
38
+ height_ratios: Relative heights for each panel.
39
+ figsize: Figure size as (width, height).
40
+ sharex: Whether panels share the x-axis.
41
+
42
+ Returns:
43
+ Tuple of (figure, list of axes).
44
+ """
45
+ # Prevent auto-display in interactive environments
46
+ plt.ioff()
47
+
48
+ if n_panels == 1:
49
+ fig, ax = plt.subplots(figsize=figsize)
50
+ return fig, [ax]
51
+
52
+ fig, axes = plt.subplots(
53
+ n_panels,
54
+ 1,
55
+ figsize=figsize,
56
+ height_ratios=height_ratios,
57
+ sharex=sharex,
58
+ gridspec_kw={"hspace": 0},
59
+ )
60
+
61
+ # Ensure axes is always a list
62
+ if n_panels == 1:
63
+ axes = [axes]
64
+
65
+ return fig, list(axes)
66
+
67
+ def scatter(
68
+ self,
69
+ ax: Axes,
70
+ x: pd.Series,
71
+ y: pd.Series,
72
+ colors: Union[str, List[str], pd.Series],
73
+ sizes: Union[float, List[float], pd.Series] = 60,
74
+ marker: str = "o",
75
+ edgecolor: str = "black",
76
+ linewidth: float = 0.5,
77
+ zorder: int = 2,
78
+ hover_data: Optional[pd.DataFrame] = None,
79
+ label: Optional[str] = None,
80
+ ) -> Any:
81
+ """Create a scatter plot on the given axes.
82
+
83
+ Note: hover_data is ignored for matplotlib (static plots).
84
+ """
85
+ return ax.scatter(
86
+ x,
87
+ y,
88
+ c=colors,
89
+ s=sizes,
90
+ marker=marker,
91
+ edgecolor=edgecolor,
92
+ linewidth=linewidth,
93
+ zorder=zorder,
94
+ label=label,
95
+ )
96
+
97
+ def line(
98
+ self,
99
+ ax: Axes,
100
+ x: pd.Series,
101
+ y: pd.Series,
102
+ color: str = "blue",
103
+ linewidth: float = 1.5,
104
+ alpha: float = 1.0,
105
+ linestyle: str = "-",
106
+ zorder: int = 1,
107
+ label: Optional[str] = None,
108
+ ) -> Any:
109
+ """Create a line plot on the given axes."""
110
+ (line,) = ax.plot(
111
+ x,
112
+ y,
113
+ color=color,
114
+ linewidth=linewidth,
115
+ alpha=alpha,
116
+ linestyle=linestyle,
117
+ zorder=zorder,
118
+ label=label,
119
+ )
120
+ return line
121
+
122
+ def fill_between(
123
+ self,
124
+ ax: Axes,
125
+ x: pd.Series,
126
+ y1: Union[float, pd.Series],
127
+ y2: Union[float, pd.Series],
128
+ color: str = "blue",
129
+ alpha: float = 0.3,
130
+ zorder: int = 0,
131
+ ) -> Any:
132
+ """Fill area between two y-values."""
133
+ return ax.fill_between(x, y1, y2, color=color, alpha=alpha, zorder=zorder)
134
+
135
+ def axhline(
136
+ self,
137
+ ax: Axes,
138
+ y: float,
139
+ color: str = "grey",
140
+ linestyle: str = "--",
141
+ linewidth: float = 1.0,
142
+ zorder: int = 1,
143
+ ) -> Any:
144
+ """Add a horizontal line across the axes."""
145
+ return ax.axhline(
146
+ y=y, color=color, linestyle=linestyle, linewidth=linewidth, zorder=zorder
147
+ )
148
+
149
+ def add_text(
150
+ self,
151
+ ax: Axes,
152
+ x: float,
153
+ y: float,
154
+ text: str,
155
+ fontsize: int = 10,
156
+ ha: str = "center",
157
+ va: str = "bottom",
158
+ rotation: float = 0,
159
+ color: str = "black",
160
+ ) -> Any:
161
+ """Add text annotation to axes."""
162
+ return ax.text(
163
+ x, y, text, fontsize=fontsize, ha=ha, va=va, rotation=rotation, color=color
164
+ )
165
+
166
+ def add_rectangle(
167
+ self,
168
+ ax: Axes,
169
+ xy: Tuple[float, float],
170
+ width: float,
171
+ height: float,
172
+ facecolor: str = "blue",
173
+ edgecolor: str = "black",
174
+ linewidth: float = 0.5,
175
+ zorder: int = 2,
176
+ ) -> Any:
177
+ """Add a rectangle patch to axes."""
178
+ rect = Rectangle(
179
+ xy,
180
+ width,
181
+ height,
182
+ facecolor=facecolor,
183
+ edgecolor=edgecolor,
184
+ linewidth=linewidth,
185
+ zorder=zorder,
186
+ )
187
+ ax.add_patch(rect)
188
+ return rect
189
+
190
+ def set_xlim(self, ax: Axes, left: float, right: float) -> None:
191
+ """Set x-axis limits."""
192
+ ax.set_xlim(left, right)
193
+
194
+ def set_ylim(self, ax: Axes, bottom: float, top: float) -> None:
195
+ """Set y-axis limits."""
196
+ ax.set_ylim(bottom, top)
197
+
198
+ def set_xlabel(self, ax: Axes, label: str, fontsize: int = 12) -> None:
199
+ """Set x-axis label."""
200
+ ax.set_xlabel(label, fontsize=fontsize)
201
+
202
+ def set_ylabel(self, ax: Axes, label: str, fontsize: int = 12) -> None:
203
+ """Set y-axis label."""
204
+ ax.set_ylabel(label, fontsize=fontsize)
205
+
206
+ def set_title(self, ax: Axes, title: str, fontsize: int = 14) -> None:
207
+ """Set panel title."""
208
+ ax.set_title(title, fontsize=fontsize)
209
+
210
+ def create_twin_axis(self, ax: Axes) -> Axes:
211
+ """Create a secondary y-axis sharing the same x-axis."""
212
+ return ax.twinx()
213
+
214
+ def add_legend(
215
+ self,
216
+ ax: Axes,
217
+ handles: List[Any],
218
+ labels: List[str],
219
+ loc: str = "upper left",
220
+ title: Optional[str] = None,
221
+ ) -> Any:
222
+ """Add a legend to the axes."""
223
+ return ax.legend(
224
+ handles=handles,
225
+ labels=labels,
226
+ loc=loc,
227
+ title=title,
228
+ fontsize=9,
229
+ frameon=True,
230
+ framealpha=0.9,
231
+ title_fontsize=10,
232
+ handlelength=1.5,
233
+ handleheight=1.0,
234
+ labelspacing=0.4,
235
+ )
236
+
237
+ def hide_spines(self, ax: Axes, spines: List[str]) -> None:
238
+ """Hide specified axis spines."""
239
+ for spine in spines:
240
+ ax.spines[spine].set_visible(False)
241
+
242
+ def format_xaxis_mb(self, ax: Axes) -> None:
243
+ """Format x-axis to show megabase values."""
244
+ ax.xaxis.set_major_formatter(FuncFormatter(lambda x, _: f"{x / 1e6:.2f}"))
245
+ ax.xaxis.set_major_locator(MaxNLocator(nbins=6))
246
+
247
+ def save(
248
+ self,
249
+ fig: Figure,
250
+ path: str,
251
+ dpi: int = 150,
252
+ bbox_inches: str = "tight",
253
+ ) -> None:
254
+ """Save figure to file."""
255
+ fig.savefig(path, dpi=dpi, bbox_inches=bbox_inches)
256
+
257
+ def show(self, fig: Figure) -> None:
258
+ """Display the figure."""
259
+ plt.ion()
260
+ plt.show()
261
+
262
+ def close(self, fig: Figure) -> None:
263
+ """Close the figure and free resources."""
264
+ plt.close(fig)
265
+
266
+ def finalize_layout(
267
+ self,
268
+ fig: Figure,
269
+ left: float = 0.08,
270
+ right: float = 0.95,
271
+ top: float = 0.95,
272
+ bottom: float = 0.1,
273
+ hspace: float = 0.08,
274
+ ) -> None:
275
+ """Adjust subplot layout parameters.
276
+
277
+ Args:
278
+ fig: Figure object.
279
+ left: Left margin.
280
+ right: Right margin.
281
+ top: Top margin.
282
+ bottom: Bottom margin.
283
+ hspace: Height space between subplots.
284
+ """
285
+ fig.subplots_adjust(
286
+ left=left, right=right, top=top, bottom=bottom, hspace=hspace
287
+ )
288
+ plt.ion()