pylocuszoom 0.5.0__py3-none-any.whl → 0.8.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.
- pylocuszoom/__init__.py +38 -2
- pylocuszoom/backends/__init__.py +116 -17
- pylocuszoom/backends/base.py +424 -35
- pylocuszoom/backends/bokeh_backend.py +192 -34
- pylocuszoom/backends/hover.py +198 -0
- pylocuszoom/backends/matplotlib_backend.py +332 -3
- pylocuszoom/backends/plotly_backend.py +187 -38
- pylocuszoom/colors.py +41 -0
- pylocuszoom/ensembl.py +476 -0
- pylocuszoom/eqtl.py +15 -19
- pylocuszoom/finemapping.py +17 -26
- pylocuszoom/forest.py +35 -0
- pylocuszoom/gene_track.py +161 -135
- pylocuszoom/loaders.py +38 -18
- pylocuszoom/phewas.py +34 -0
- pylocuszoom/plotter.py +370 -190
- pylocuszoom/recombination.py +64 -34
- pylocuszoom/schemas.py +37 -26
- pylocuszoom/utils.py +52 -0
- pylocuszoom/validation.py +172 -0
- {pylocuszoom-0.5.0.dist-info → pylocuszoom-0.8.0.dist-info}/METADATA +97 -28
- pylocuszoom-0.8.0.dist-info/RECORD +29 -0
- pylocuszoom-0.5.0.dist-info/RECORD +0 -24
- {pylocuszoom-0.5.0.dist-info → pylocuszoom-0.8.0.dist-info}/WHEEL +0 -0
- {pylocuszoom-0.5.0.dist-info → pylocuszoom-0.8.0.dist-info}/licenses/LICENSE.md +0 -0
|
@@ -12,17 +12,40 @@ from matplotlib.figure import Figure
|
|
|
12
12
|
from matplotlib.patches import Polygon, Rectangle
|
|
13
13
|
from matplotlib.ticker import FuncFormatter, MaxNLocator
|
|
14
14
|
|
|
15
|
+
from . import register_backend
|
|
15
16
|
|
|
17
|
+
|
|
18
|
+
@register_backend("matplotlib")
|
|
16
19
|
class MatplotlibBackend:
|
|
17
20
|
"""Matplotlib backend for static plot generation.
|
|
18
21
|
|
|
19
22
|
This is the default backend, producing publication-quality static plots
|
|
20
23
|
suitable for papers and presentations.
|
|
24
|
+
|
|
25
|
+
Capability Properties:
|
|
26
|
+
supports_snp_labels: True - uses adjustText for automatic label positioning.
|
|
27
|
+
supports_hover: False - static plots don't support hover tooltips.
|
|
28
|
+
supports_secondary_axis: True - supports twin y-axis via twinx().
|
|
21
29
|
"""
|
|
22
30
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
31
|
+
# =========================================================================
|
|
32
|
+
# Capability Properties
|
|
33
|
+
# =========================================================================
|
|
34
|
+
|
|
35
|
+
@property
|
|
36
|
+
def supports_snp_labels(self) -> bool:
|
|
37
|
+
"""Matplotlib supports SNP labels via adjustText."""
|
|
38
|
+
return True
|
|
39
|
+
|
|
40
|
+
@property
|
|
41
|
+
def supports_hover(self) -> bool:
|
|
42
|
+
"""Matplotlib does not support hover tooltips."""
|
|
43
|
+
return False
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
def supports_secondary_axis(self) -> bool:
|
|
47
|
+
"""Matplotlib supports twin y-axis."""
|
|
48
|
+
return True
|
|
26
49
|
|
|
27
50
|
def create_figure(
|
|
28
51
|
self,
|
|
@@ -164,6 +187,67 @@ class MatplotlibBackend:
|
|
|
164
187
|
x, y, text, fontsize=fontsize, ha=ha, va=va, rotation=rotation, color=color
|
|
165
188
|
)
|
|
166
189
|
|
|
190
|
+
def add_panel_label(
|
|
191
|
+
self,
|
|
192
|
+
ax: Axes,
|
|
193
|
+
label: str,
|
|
194
|
+
x_frac: float = 0.02,
|
|
195
|
+
y_frac: float = 0.95,
|
|
196
|
+
) -> None:
|
|
197
|
+
"""Add label text at fractional position in panel.
|
|
198
|
+
|
|
199
|
+
Args:
|
|
200
|
+
ax: Matplotlib axes.
|
|
201
|
+
label: Label text (e.g., "A", "B").
|
|
202
|
+
x_frac: Horizontal position as fraction of axes (0-1).
|
|
203
|
+
y_frac: Vertical position as fraction of axes (0-1).
|
|
204
|
+
"""
|
|
205
|
+
ax.annotate(
|
|
206
|
+
label,
|
|
207
|
+
xy=(x_frac, y_frac),
|
|
208
|
+
xycoords="axes fraction",
|
|
209
|
+
fontsize=10,
|
|
210
|
+
fontweight="bold",
|
|
211
|
+
ha="left",
|
|
212
|
+
va="top",
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
def add_snp_labels(
|
|
216
|
+
self,
|
|
217
|
+
ax: Axes,
|
|
218
|
+
df: pd.DataFrame,
|
|
219
|
+
pos_col: str,
|
|
220
|
+
neglog10p_col: str,
|
|
221
|
+
rs_col: str,
|
|
222
|
+
label_top_n: int,
|
|
223
|
+
genes_df: Optional[pd.DataFrame],
|
|
224
|
+
chrom: int,
|
|
225
|
+
) -> None:
|
|
226
|
+
"""Add SNP labels using adjustText.
|
|
227
|
+
|
|
228
|
+
Args:
|
|
229
|
+
ax: Matplotlib axes.
|
|
230
|
+
df: DataFrame with SNP data.
|
|
231
|
+
pos_col: Column name for position.
|
|
232
|
+
neglog10p_col: Column name for -log10(p-value).
|
|
233
|
+
rs_col: Column name for SNP ID.
|
|
234
|
+
label_top_n: Number of top SNPs to label.
|
|
235
|
+
genes_df: Gene annotations (unused, for signature compatibility).
|
|
236
|
+
chrom: Chromosome number (unused, for signature compatibility).
|
|
237
|
+
"""
|
|
238
|
+
from ..labels import add_snp_labels as _add_snp_labels
|
|
239
|
+
|
|
240
|
+
_add_snp_labels(
|
|
241
|
+
ax,
|
|
242
|
+
df,
|
|
243
|
+
pos_col=pos_col,
|
|
244
|
+
neglog10p_col=neglog10p_col,
|
|
245
|
+
rs_col=rs_col,
|
|
246
|
+
label_top_n=label_top_n,
|
|
247
|
+
genes_df=genes_df,
|
|
248
|
+
chrom=chrom,
|
|
249
|
+
)
|
|
250
|
+
|
|
167
251
|
def add_rectangle(
|
|
168
252
|
self,
|
|
169
253
|
ax: Axes,
|
|
@@ -225,6 +309,17 @@ class MatplotlibBackend:
|
|
|
225
309
|
"""Set y-axis label."""
|
|
226
310
|
ax.set_ylabel(label, fontsize=fontsize)
|
|
227
311
|
|
|
312
|
+
def set_yticks(
|
|
313
|
+
self,
|
|
314
|
+
ax: Axes,
|
|
315
|
+
positions: List[float],
|
|
316
|
+
labels: List[str],
|
|
317
|
+
fontsize: int = 10,
|
|
318
|
+
) -> None:
|
|
319
|
+
"""Set y-axis tick positions and labels."""
|
|
320
|
+
ax.set_yticks(positions)
|
|
321
|
+
ax.set_yticklabels(labels, fontsize=fontsize)
|
|
322
|
+
|
|
228
323
|
def set_title(self, ax: Axes, title: str, fontsize: int = 14) -> None:
|
|
229
324
|
"""Set panel title."""
|
|
230
325
|
ax.set_title(
|
|
@@ -238,6 +333,119 @@ class MatplotlibBackend:
|
|
|
238
333
|
"""Create a secondary y-axis sharing the same x-axis."""
|
|
239
334
|
return ax.twinx()
|
|
240
335
|
|
|
336
|
+
def line_secondary(
|
|
337
|
+
self,
|
|
338
|
+
ax: Axes,
|
|
339
|
+
x: pd.Series,
|
|
340
|
+
y: pd.Series,
|
|
341
|
+
color: str = "blue",
|
|
342
|
+
linewidth: float = 1.5,
|
|
343
|
+
alpha: float = 1.0,
|
|
344
|
+
linestyle: str = "-",
|
|
345
|
+
label: Optional[str] = None,
|
|
346
|
+
yaxis_name: Any = None,
|
|
347
|
+
) -> Any:
|
|
348
|
+
"""Create line on secondary y-axis.
|
|
349
|
+
|
|
350
|
+
For matplotlib, the ax should already be a twin axis from create_twin_axis().
|
|
351
|
+
The yaxis_name parameter is ignored (provided for interface compatibility).
|
|
352
|
+
|
|
353
|
+
Args:
|
|
354
|
+
ax: Secondary axes from create_twin_axis().
|
|
355
|
+
x: X-axis values.
|
|
356
|
+
y: Y-axis values.
|
|
357
|
+
color: Line color.
|
|
358
|
+
linewidth: Line width.
|
|
359
|
+
alpha: Transparency.
|
|
360
|
+
linestyle: Line style.
|
|
361
|
+
label: Legend label.
|
|
362
|
+
yaxis_name: Ignored for matplotlib.
|
|
363
|
+
|
|
364
|
+
Returns:
|
|
365
|
+
The line object.
|
|
366
|
+
"""
|
|
367
|
+
return self.line(
|
|
368
|
+
ax,
|
|
369
|
+
x,
|
|
370
|
+
y,
|
|
371
|
+
color=color,
|
|
372
|
+
linewidth=linewidth,
|
|
373
|
+
alpha=alpha,
|
|
374
|
+
linestyle=linestyle,
|
|
375
|
+
label=label,
|
|
376
|
+
)
|
|
377
|
+
|
|
378
|
+
def fill_between_secondary(
|
|
379
|
+
self,
|
|
380
|
+
ax: Axes,
|
|
381
|
+
x: pd.Series,
|
|
382
|
+
y1: Union[float, pd.Series],
|
|
383
|
+
y2: Union[float, pd.Series],
|
|
384
|
+
color: str = "blue",
|
|
385
|
+
alpha: float = 0.3,
|
|
386
|
+
yaxis_name: Any = None,
|
|
387
|
+
) -> Any:
|
|
388
|
+
"""Fill area on secondary y-axis.
|
|
389
|
+
|
|
390
|
+
For matplotlib, the ax should already be a twin axis from create_twin_axis().
|
|
391
|
+
The yaxis_name parameter is ignored (provided for interface compatibility).
|
|
392
|
+
|
|
393
|
+
Args:
|
|
394
|
+
ax: Secondary axes from create_twin_axis().
|
|
395
|
+
x: X-axis values.
|
|
396
|
+
y1: Lower y boundary.
|
|
397
|
+
y2: Upper y boundary.
|
|
398
|
+
color: Fill color.
|
|
399
|
+
alpha: Transparency.
|
|
400
|
+
yaxis_name: Ignored for matplotlib.
|
|
401
|
+
|
|
402
|
+
Returns:
|
|
403
|
+
The fill object.
|
|
404
|
+
"""
|
|
405
|
+
return self.fill_between(ax, x, y1, y2, color=color, alpha=alpha)
|
|
406
|
+
|
|
407
|
+
def set_secondary_ylim(
|
|
408
|
+
self,
|
|
409
|
+
ax: Axes,
|
|
410
|
+
bottom: float,
|
|
411
|
+
top: float,
|
|
412
|
+
yaxis_name: Any = None,
|
|
413
|
+
) -> None:
|
|
414
|
+
"""Set secondary y-axis limits.
|
|
415
|
+
|
|
416
|
+
For matplotlib, the ax should already be a twin axis from create_twin_axis().
|
|
417
|
+
The yaxis_name parameter is ignored (provided for interface compatibility).
|
|
418
|
+
|
|
419
|
+
Args:
|
|
420
|
+
ax: Secondary axes from create_twin_axis().
|
|
421
|
+
bottom: Minimum y value.
|
|
422
|
+
top: Maximum y value.
|
|
423
|
+
yaxis_name: Ignored for matplotlib.
|
|
424
|
+
"""
|
|
425
|
+
self.set_ylim(ax, bottom, top)
|
|
426
|
+
|
|
427
|
+
def set_secondary_ylabel(
|
|
428
|
+
self,
|
|
429
|
+
ax: Axes,
|
|
430
|
+
label: str,
|
|
431
|
+
color: str = "black",
|
|
432
|
+
fontsize: int = 10,
|
|
433
|
+
yaxis_name: Any = None,
|
|
434
|
+
) -> None:
|
|
435
|
+
"""Set secondary y-axis label.
|
|
436
|
+
|
|
437
|
+
For matplotlib, the ax should already be a twin axis from create_twin_axis().
|
|
438
|
+
The yaxis_name parameter is ignored (provided for interface compatibility).
|
|
439
|
+
|
|
440
|
+
Args:
|
|
441
|
+
ax: Secondary axes from create_twin_axis().
|
|
442
|
+
label: Label text.
|
|
443
|
+
color: Label color.
|
|
444
|
+
fontsize: Font size.
|
|
445
|
+
yaxis_name: Ignored for matplotlib.
|
|
446
|
+
"""
|
|
447
|
+
ax.set_ylabel(label, fontsize=fontsize, color=color)
|
|
448
|
+
|
|
241
449
|
def add_legend(
|
|
242
450
|
self,
|
|
243
451
|
ax: Axes,
|
|
@@ -266,6 +474,10 @@ class MatplotlibBackend:
|
|
|
266
474
|
for spine in spines:
|
|
267
475
|
ax.spines[spine].set_visible(False)
|
|
268
476
|
|
|
477
|
+
def hide_yaxis(self, ax: Axes) -> None:
|
|
478
|
+
"""Hide y-axis ticks, labels, and line."""
|
|
479
|
+
ax.yaxis.set_visible(False)
|
|
480
|
+
|
|
269
481
|
def format_xaxis_mb(self, ax: Axes) -> None:
|
|
270
482
|
"""Format x-axis to show megabase values."""
|
|
271
483
|
ax.xaxis.set_major_formatter(FuncFormatter(lambda x, _: f"{x / 1e6:.2f}"))
|
|
@@ -394,6 +606,123 @@ class MatplotlibBackend:
|
|
|
394
606
|
"""Add simple legend for labeled scatter data."""
|
|
395
607
|
ax.legend(loc=loc, fontsize=9)
|
|
396
608
|
|
|
609
|
+
def add_ld_legend(
|
|
610
|
+
self,
|
|
611
|
+
ax: Axes,
|
|
612
|
+
ld_bins: List[Tuple[float, str, str]],
|
|
613
|
+
lead_snp_color: str,
|
|
614
|
+
) -> None:
|
|
615
|
+
"""Add LD color legend using matplotlib patches.
|
|
616
|
+
|
|
617
|
+
Args:
|
|
618
|
+
ax: Matplotlib axes.
|
|
619
|
+
ld_bins: List of (threshold, label, color) tuples defining LD bins.
|
|
620
|
+
lead_snp_color: Color for lead SNP marker in legend.
|
|
621
|
+
"""
|
|
622
|
+
from matplotlib.lines import Line2D
|
|
623
|
+
from matplotlib.patches import Patch
|
|
624
|
+
|
|
625
|
+
from ..colors import get_ld_color_palette
|
|
626
|
+
|
|
627
|
+
palette = get_ld_color_palette()
|
|
628
|
+
legend_elements = [
|
|
629
|
+
Line2D(
|
|
630
|
+
[0],
|
|
631
|
+
[0],
|
|
632
|
+
marker="D",
|
|
633
|
+
color="w",
|
|
634
|
+
markerfacecolor=lead_snp_color,
|
|
635
|
+
markeredgecolor="black",
|
|
636
|
+
markersize=6,
|
|
637
|
+
label="Lead SNP",
|
|
638
|
+
),
|
|
639
|
+
]
|
|
640
|
+
for _threshold, label, _color in ld_bins:
|
|
641
|
+
legend_elements.append(
|
|
642
|
+
Patch(facecolor=palette[label], edgecolor="black", label=label)
|
|
643
|
+
)
|
|
644
|
+
ax.legend(
|
|
645
|
+
handles=legend_elements,
|
|
646
|
+
loc="upper right",
|
|
647
|
+
fontsize=9,
|
|
648
|
+
frameon=True,
|
|
649
|
+
framealpha=0.9,
|
|
650
|
+
title=r"$r^2$",
|
|
651
|
+
title_fontsize=10,
|
|
652
|
+
handlelength=1.5,
|
|
653
|
+
handleheight=1.0,
|
|
654
|
+
labelspacing=0.4,
|
|
655
|
+
)
|
|
656
|
+
|
|
657
|
+
def axvline(
|
|
658
|
+
self,
|
|
659
|
+
ax: Axes,
|
|
660
|
+
x: float,
|
|
661
|
+
color: str = "grey",
|
|
662
|
+
linestyle: str = "--",
|
|
663
|
+
linewidth: float = 1.0,
|
|
664
|
+
alpha: float = 1.0,
|
|
665
|
+
zorder: int = 1,
|
|
666
|
+
) -> Any:
|
|
667
|
+
"""Add a vertical line across the axes."""
|
|
668
|
+
return ax.axvline(
|
|
669
|
+
x=x,
|
|
670
|
+
color=color,
|
|
671
|
+
linestyle=linestyle,
|
|
672
|
+
linewidth=linewidth,
|
|
673
|
+
alpha=alpha,
|
|
674
|
+
zorder=zorder,
|
|
675
|
+
)
|
|
676
|
+
|
|
677
|
+
def hbar(
|
|
678
|
+
self,
|
|
679
|
+
ax: Axes,
|
|
680
|
+
y: pd.Series,
|
|
681
|
+
width: pd.Series,
|
|
682
|
+
height: float = 0.8,
|
|
683
|
+
left: Union[float, pd.Series] = 0,
|
|
684
|
+
color: Union[str, List[str]] = "blue",
|
|
685
|
+
edgecolor: str = "black",
|
|
686
|
+
linewidth: float = 0.5,
|
|
687
|
+
zorder: int = 2,
|
|
688
|
+
) -> Any:
|
|
689
|
+
"""Create horizontal bar chart."""
|
|
690
|
+
return ax.barh(
|
|
691
|
+
y=y,
|
|
692
|
+
width=width,
|
|
693
|
+
height=height,
|
|
694
|
+
left=left,
|
|
695
|
+
color=color,
|
|
696
|
+
edgecolor=edgecolor,
|
|
697
|
+
linewidth=linewidth,
|
|
698
|
+
zorder=zorder,
|
|
699
|
+
)
|
|
700
|
+
|
|
701
|
+
def errorbar_h(
|
|
702
|
+
self,
|
|
703
|
+
ax: Axes,
|
|
704
|
+
x: pd.Series,
|
|
705
|
+
y: pd.Series,
|
|
706
|
+
xerr_lower: pd.Series,
|
|
707
|
+
xerr_upper: pd.Series,
|
|
708
|
+
color: str = "black",
|
|
709
|
+
linewidth: float = 1.5,
|
|
710
|
+
capsize: float = 3,
|
|
711
|
+
zorder: int = 3,
|
|
712
|
+
) -> Any:
|
|
713
|
+
"""Add horizontal error bars."""
|
|
714
|
+
xerr = [xerr_lower.values, xerr_upper.values]
|
|
715
|
+
return ax.errorbar(
|
|
716
|
+
x=x,
|
|
717
|
+
y=y,
|
|
718
|
+
xerr=xerr,
|
|
719
|
+
fmt="none",
|
|
720
|
+
ecolor=color,
|
|
721
|
+
elinewidth=linewidth,
|
|
722
|
+
capsize=capsize,
|
|
723
|
+
zorder=zorder,
|
|
724
|
+
)
|
|
725
|
+
|
|
397
726
|
def finalize_layout(
|
|
398
727
|
self,
|
|
399
728
|
fig: Figure,
|
|
@@ -9,7 +9,10 @@ import pandas as pd
|
|
|
9
9
|
import plotly.graph_objects as go
|
|
10
10
|
from plotly.subplots import make_subplots
|
|
11
11
|
|
|
12
|
+
from . import convert_latex_to_unicode, register_backend
|
|
12
13
|
|
|
14
|
+
|
|
15
|
+
@register_backend("plotly")
|
|
13
16
|
class PlotlyBackend:
|
|
14
17
|
"""Plotly backend for interactive plot generation.
|
|
15
18
|
|
|
@@ -20,15 +23,35 @@ class PlotlyBackend:
|
|
|
20
23
|
- Nearest gene
|
|
21
24
|
"""
|
|
22
25
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
26
|
+
# Class constants for style mappings
|
|
27
|
+
_MARKER_SYMBOLS = {
|
|
28
|
+
"o": "circle",
|
|
29
|
+
"D": "diamond",
|
|
30
|
+
"s": "square",
|
|
31
|
+
"^": "triangle-up",
|
|
32
|
+
"v": "triangle-down",
|
|
33
|
+
}
|
|
34
|
+
_DASH_MAP = {
|
|
35
|
+
"-": "solid",
|
|
36
|
+
"--": "dash",
|
|
37
|
+
":": "dot",
|
|
38
|
+
"-.": "dashdot",
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
@property
|
|
42
|
+
def supports_snp_labels(self) -> bool:
|
|
43
|
+
"""Plotly uses hover tooltips instead of labels."""
|
|
44
|
+
return False
|
|
45
|
+
|
|
46
|
+
@property
|
|
47
|
+
def supports_hover(self) -> bool:
|
|
48
|
+
"""Plotly supports hover tooltips."""
|
|
49
|
+
return True
|
|
50
|
+
|
|
51
|
+
@property
|
|
52
|
+
def supports_secondary_axis(self) -> bool:
|
|
53
|
+
"""Plotly supports secondary y-axis."""
|
|
54
|
+
return True
|
|
32
55
|
|
|
33
56
|
def create_figure(
|
|
34
57
|
self,
|
|
@@ -111,7 +134,7 @@ class PlotlyBackend:
|
|
|
111
134
|
fig, row = ax
|
|
112
135
|
|
|
113
136
|
# Convert matplotlib marker to plotly symbol
|
|
114
|
-
symbol = self.
|
|
137
|
+
symbol = self._MARKER_SYMBOLS.get(marker, "circle")
|
|
115
138
|
|
|
116
139
|
# Convert size (matplotlib uses area, plotly uses diameter)
|
|
117
140
|
if isinstance(sizes, (int, float)):
|
|
@@ -178,15 +201,7 @@ class PlotlyBackend:
|
|
|
178
201
|
) -> Any:
|
|
179
202
|
"""Create a line plot on the given panel."""
|
|
180
203
|
fig, row = ax
|
|
181
|
-
|
|
182
|
-
# Convert linestyle
|
|
183
|
-
dash_map = {
|
|
184
|
-
"-": "solid",
|
|
185
|
-
"--": "dash",
|
|
186
|
-
":": "dot",
|
|
187
|
-
"-.": "dashdot",
|
|
188
|
-
}
|
|
189
|
-
dash = dash_map.get(linestyle, "solid")
|
|
204
|
+
dash = self._DASH_MAP.get(linestyle, "solid")
|
|
190
205
|
|
|
191
206
|
trace = go.Scatter(
|
|
192
207
|
x=x,
|
|
@@ -244,9 +259,7 @@ class PlotlyBackend:
|
|
|
244
259
|
) -> Any:
|
|
245
260
|
"""Add a horizontal line across the panel."""
|
|
246
261
|
fig, row = ax
|
|
247
|
-
|
|
248
|
-
dash_map = {"-": "solid", "--": "dash", ":": "dot", "-.": "dashdot"}
|
|
249
|
-
dash = dash_map.get(linestyle, "dash")
|
|
262
|
+
dash = self._DASH_MAP.get(linestyle, "dash")
|
|
250
263
|
|
|
251
264
|
fig.add_hline(
|
|
252
265
|
y=y,
|
|
@@ -384,6 +397,26 @@ class PlotlyBackend:
|
|
|
384
397
|
}
|
|
385
398
|
)
|
|
386
399
|
|
|
400
|
+
def set_yticks(
|
|
401
|
+
self,
|
|
402
|
+
ax: Tuple[go.Figure, int],
|
|
403
|
+
positions: List[float],
|
|
404
|
+
labels: List[str],
|
|
405
|
+
fontsize: int = 10,
|
|
406
|
+
) -> None:
|
|
407
|
+
"""Set y-axis tick positions and labels."""
|
|
408
|
+
fig, row = ax
|
|
409
|
+
fig.update_layout(
|
|
410
|
+
**{
|
|
411
|
+
self._axis_name("yaxis", row): dict(
|
|
412
|
+
tickmode="array",
|
|
413
|
+
tickvals=positions,
|
|
414
|
+
ticktext=labels,
|
|
415
|
+
tickfont=dict(size=fontsize),
|
|
416
|
+
)
|
|
417
|
+
}
|
|
418
|
+
)
|
|
419
|
+
|
|
387
420
|
def _axis_name(self, axis: str, row: int) -> str:
|
|
388
421
|
"""Get Plotly axis name for a given row.
|
|
389
422
|
|
|
@@ -404,19 +437,7 @@ class PlotlyBackend:
|
|
|
404
437
|
|
|
405
438
|
def _convert_label(self, label: str) -> str:
|
|
406
439
|
"""Convert LaTeX-style labels to Unicode for Plotly display."""
|
|
407
|
-
|
|
408
|
-
(r"$-\log_{10}$ P", "-log₁₀(P)"),
|
|
409
|
-
(r"$-\log_{10}$", "-log₁₀"),
|
|
410
|
-
(r"\log_{10}", "log₁₀"),
|
|
411
|
-
(r"$r^2$", "r²"),
|
|
412
|
-
(r"$R^2$", "R²"),
|
|
413
|
-
]
|
|
414
|
-
for latex, unicode_str in conversions:
|
|
415
|
-
if latex in label:
|
|
416
|
-
label = label.replace(latex, unicode_str)
|
|
417
|
-
# Remove any remaining $ markers
|
|
418
|
-
label = label.replace("$", "")
|
|
419
|
-
return label
|
|
440
|
+
return convert_latex_to_unicode(label)
|
|
420
441
|
|
|
421
442
|
def set_title(
|
|
422
443
|
self, ax: Tuple[go.Figure, int], title: str, fontsize: int = 14
|
|
@@ -461,9 +482,7 @@ class PlotlyBackend:
|
|
|
461
482
|
) -> Any:
|
|
462
483
|
"""Create a line plot on secondary y-axis."""
|
|
463
484
|
fig, row = ax
|
|
464
|
-
|
|
465
|
-
dash_map = {"-": "solid", "--": "dash", ":": "dot", "-.": "dashdot"}
|
|
466
|
-
dash = dash_map.get(linestyle, "solid")
|
|
485
|
+
dash = self._DASH_MAP.get(linestyle, "solid")
|
|
467
486
|
|
|
468
487
|
trace = go.Scatter(
|
|
469
488
|
x=x,
|
|
@@ -609,6 +628,41 @@ class PlotlyBackend:
|
|
|
609
628
|
}
|
|
610
629
|
)
|
|
611
630
|
|
|
631
|
+
def add_snp_labels(
|
|
632
|
+
self,
|
|
633
|
+
ax: Tuple[go.Figure, int],
|
|
634
|
+
df: pd.DataFrame,
|
|
635
|
+
pos_col: str,
|
|
636
|
+
neglog10p_col: str,
|
|
637
|
+
rs_col: str,
|
|
638
|
+
label_top_n: int,
|
|
639
|
+
genes_df: Optional[pd.DataFrame],
|
|
640
|
+
chrom: int,
|
|
641
|
+
) -> None:
|
|
642
|
+
"""No-op: Plotly uses hover tooltips instead of text labels."""
|
|
643
|
+
pass
|
|
644
|
+
|
|
645
|
+
def add_panel_label(
|
|
646
|
+
self,
|
|
647
|
+
ax: Tuple[go.Figure, int],
|
|
648
|
+
label: str,
|
|
649
|
+
x_frac: float = 0.02,
|
|
650
|
+
y_frac: float = 0.95,
|
|
651
|
+
) -> None:
|
|
652
|
+
"""Add label text at fractional position in panel."""
|
|
653
|
+
fig, row = ax
|
|
654
|
+
fig.add_annotation(
|
|
655
|
+
text=f"<b>{label}</b>",
|
|
656
|
+
xref="x domain",
|
|
657
|
+
yref="y domain",
|
|
658
|
+
x=x_frac,
|
|
659
|
+
y=y_frac,
|
|
660
|
+
showarrow=False,
|
|
661
|
+
font=dict(size=12),
|
|
662
|
+
row=row,
|
|
663
|
+
col=1,
|
|
664
|
+
)
|
|
665
|
+
|
|
612
666
|
def add_ld_legend(
|
|
613
667
|
self,
|
|
614
668
|
ax: Tuple[go.Figure, int],
|
|
@@ -782,6 +836,101 @@ class PlotlyBackend:
|
|
|
782
836
|
)
|
|
783
837
|
)
|
|
784
838
|
|
|
839
|
+
def axvline(
|
|
840
|
+
self,
|
|
841
|
+
ax: Tuple[go.Figure, int],
|
|
842
|
+
x: float,
|
|
843
|
+
color: str = "grey",
|
|
844
|
+
linestyle: str = "--",
|
|
845
|
+
linewidth: float = 1.0,
|
|
846
|
+
alpha: float = 1.0,
|
|
847
|
+
zorder: int = 1,
|
|
848
|
+
) -> Any:
|
|
849
|
+
"""Add a vertical line across the panel."""
|
|
850
|
+
fig, row = ax
|
|
851
|
+
dash = self._DASH_MAP.get(linestyle, "dash")
|
|
852
|
+
|
|
853
|
+
fig.add_vline(
|
|
854
|
+
x=x,
|
|
855
|
+
line_dash=dash,
|
|
856
|
+
line_color=color,
|
|
857
|
+
line_width=linewidth,
|
|
858
|
+
opacity=alpha,
|
|
859
|
+
row=row,
|
|
860
|
+
col=1,
|
|
861
|
+
)
|
|
862
|
+
|
|
863
|
+
def hbar(
|
|
864
|
+
self,
|
|
865
|
+
ax: Tuple[go.Figure, int],
|
|
866
|
+
y: pd.Series,
|
|
867
|
+
width: pd.Series,
|
|
868
|
+
height: float = 0.8,
|
|
869
|
+
left: Union[float, pd.Series] = 0,
|
|
870
|
+
color: Union[str, List[str]] = "blue",
|
|
871
|
+
edgecolor: str = "black",
|
|
872
|
+
linewidth: float = 0.5,
|
|
873
|
+
zorder: int = 2,
|
|
874
|
+
) -> Any:
|
|
875
|
+
"""Create horizontal bar chart."""
|
|
876
|
+
fig, row = ax
|
|
877
|
+
|
|
878
|
+
# Convert left to array if scalar
|
|
879
|
+
if isinstance(left, (int, float)):
|
|
880
|
+
left_arr = [left] * len(y)
|
|
881
|
+
else:
|
|
882
|
+
left_arr = list(left) if hasattr(left, "tolist") else left
|
|
883
|
+
|
|
884
|
+
trace = go.Bar(
|
|
885
|
+
y=y,
|
|
886
|
+
x=width,
|
|
887
|
+
orientation="h",
|
|
888
|
+
base=left_arr,
|
|
889
|
+
marker=dict(
|
|
890
|
+
color=color,
|
|
891
|
+
line=dict(color=edgecolor, width=linewidth),
|
|
892
|
+
),
|
|
893
|
+
showlegend=False,
|
|
894
|
+
)
|
|
895
|
+
|
|
896
|
+
fig.add_trace(trace, row=row, col=1)
|
|
897
|
+
return trace
|
|
898
|
+
|
|
899
|
+
def errorbar_h(
|
|
900
|
+
self,
|
|
901
|
+
ax: Tuple[go.Figure, int],
|
|
902
|
+
x: pd.Series,
|
|
903
|
+
y: pd.Series,
|
|
904
|
+
xerr_lower: pd.Series,
|
|
905
|
+
xerr_upper: pd.Series,
|
|
906
|
+
color: str = "black",
|
|
907
|
+
linewidth: float = 1.5,
|
|
908
|
+
capsize: float = 3,
|
|
909
|
+
zorder: int = 3,
|
|
910
|
+
) -> Any:
|
|
911
|
+
"""Add horizontal error bars."""
|
|
912
|
+
fig, row = ax
|
|
913
|
+
|
|
914
|
+
trace = go.Scatter(
|
|
915
|
+
x=x,
|
|
916
|
+
y=y,
|
|
917
|
+
mode="markers",
|
|
918
|
+
marker=dict(size=0),
|
|
919
|
+
error_x=dict(
|
|
920
|
+
type="data",
|
|
921
|
+
symmetric=False,
|
|
922
|
+
array=xerr_upper,
|
|
923
|
+
arrayminus=xerr_lower,
|
|
924
|
+
color=color,
|
|
925
|
+
thickness=linewidth,
|
|
926
|
+
width=capsize,
|
|
927
|
+
),
|
|
928
|
+
showlegend=False,
|
|
929
|
+
)
|
|
930
|
+
|
|
931
|
+
fig.add_trace(trace, row=row, col=1)
|
|
932
|
+
return trace
|
|
933
|
+
|
|
785
934
|
def finalize_layout(
|
|
786
935
|
self,
|
|
787
936
|
fig: go.Figure,
|