pylocuszoom 0.5.0__py3-none-any.whl → 0.6.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 CHANGED
@@ -34,14 +34,22 @@ Species Support:
34
34
  - Custom: User provides all reference data
35
35
  """
36
36
 
37
- __version__ = "0.3.0"
37
+ __version__ = "0.6.0"
38
38
 
39
39
  # Main plotter class
40
40
  # Backend types
41
41
  from .backends import BackendType, get_backend
42
42
 
43
43
  # Colors and LD
44
- from .colors import LEAD_SNP_COLOR, get_ld_bin, get_ld_color, get_ld_color_palette
44
+ from .colors import (
45
+ LEAD_SNP_COLOR,
46
+ PHEWAS_CATEGORY_COLORS,
47
+ get_ld_bin,
48
+ get_ld_color,
49
+ get_ld_color_palette,
50
+ get_phewas_category_color,
51
+ get_phewas_category_palette,
52
+ )
45
53
 
46
54
  # eQTL support
47
55
  from .eqtl import (
@@ -65,6 +73,9 @@ from .finemapping import (
65
73
  validate_finemapping_df,
66
74
  )
67
75
 
76
+ # Forest plot support
77
+ from .forest import validate_forest_df
78
+
68
79
  # Gene track
69
80
  from .gene_track import get_nearest_gene, plot_gene_track
70
81
 
@@ -101,6 +112,9 @@ from .loaders import (
101
112
 
102
113
  # Logging configuration
103
114
  from .logging import disable_logging, enable_logging
115
+
116
+ # PheWAS support
117
+ from .phewas import validate_phewas_df
104
118
  from .plotter import LocusZoomPlotter
105
119
 
106
120
  # Reference data management
@@ -130,7 +144,10 @@ __all__ = [
130
144
  "get_ld_color",
131
145
  "get_ld_bin",
132
146
  "get_ld_color_palette",
147
+ "get_phewas_category_color",
148
+ "get_phewas_category_palette",
133
149
  "LEAD_SNP_COLOR",
150
+ "PHEWAS_CATEGORY_COLORS",
134
151
  # Gene track
135
152
  "get_nearest_gene",
136
153
  "plot_gene_track",
@@ -164,6 +181,10 @@ __all__ = [
164
181
  # Validation & Utils
165
182
  "ValidationError",
166
183
  "to_pandas",
184
+ # PheWAS
185
+ "validate_phewas_df",
186
+ # Forest plot
187
+ "validate_forest_df",
167
188
  # GWAS loaders
168
189
  "load_gwas",
169
190
  "load_plink_assoc",
@@ -386,3 +386,89 @@ class PlotBackend(Protocol):
386
386
  loc: Legend location.
387
387
  """
388
388
  ...
389
+
390
+ def axvline(
391
+ self,
392
+ ax: Any,
393
+ x: float,
394
+ color: str = "grey",
395
+ linestyle: str = "--",
396
+ linewidth: float = 1.0,
397
+ alpha: float = 1.0,
398
+ zorder: int = 1,
399
+ ) -> Any:
400
+ """Add a vertical line across the axes.
401
+
402
+ Args:
403
+ ax: Axes or panel.
404
+ x: X-value for the line.
405
+ color: Line color.
406
+ linestyle: Line style.
407
+ linewidth: Line width.
408
+ alpha: Line transparency (0-1).
409
+ zorder: Drawing order.
410
+
411
+ Returns:
412
+ The line object.
413
+ """
414
+ ...
415
+
416
+ def hbar(
417
+ self,
418
+ ax: Any,
419
+ y: pd.Series,
420
+ width: pd.Series,
421
+ height: float = 0.8,
422
+ left: Union[float, pd.Series] = 0,
423
+ color: Union[str, List[str]] = "blue",
424
+ edgecolor: str = "black",
425
+ linewidth: float = 0.5,
426
+ zorder: int = 2,
427
+ ) -> Any:
428
+ """Create horizontal bar chart.
429
+
430
+ Args:
431
+ ax: Axes or panel.
432
+ y: Y positions for bars.
433
+ width: Bar widths (x-extent).
434
+ height: Bar height.
435
+ left: Left edge positions.
436
+ color: Bar colors.
437
+ edgecolor: Edge color.
438
+ linewidth: Edge width.
439
+ zorder: Drawing order.
440
+
441
+ Returns:
442
+ The bar collection object.
443
+ """
444
+ ...
445
+
446
+ def errorbar_h(
447
+ self,
448
+ ax: Any,
449
+ x: pd.Series,
450
+ y: pd.Series,
451
+ xerr_lower: pd.Series,
452
+ xerr_upper: pd.Series,
453
+ color: str = "black",
454
+ linewidth: float = 1.5,
455
+ capsize: float = 3,
456
+ zorder: int = 3,
457
+ ) -> Any:
458
+ """Add horizontal error bars (for forest plots).
459
+
460
+ Args:
461
+ ax: Axes or panel.
462
+ x: X positions (effect sizes).
463
+ y: Y positions.
464
+ xerr_lower: Lower error (distance from x).
465
+ xerr_upper: Upper error (distance from x).
466
+ color: Line color.
467
+ linewidth: Line width.
468
+ capsize: Cap size in points.
469
+ zorder: Drawing order.
470
+
471
+ Returns:
472
+ The errorbar object.
473
+ """
474
+ ...
@@ -19,15 +19,24 @@ class BokehBackend:
19
19
  applications and dashboards.
20
20
  """
21
21
 
22
+ # Class constants for style mappings
23
+ _MARKER_MAP = {
24
+ "o": "circle",
25
+ "D": "diamond",
26
+ "s": "square",
27
+ "^": "triangle",
28
+ "v": "inverted_triangle",
29
+ }
30
+ _DASH_MAP = {
31
+ "-": "solid",
32
+ "--": "dashed",
33
+ ":": "dotted",
34
+ "-.": "dashdot",
35
+ }
36
+
22
37
  def __init__(self) -> None:
23
38
  """Initialize the bokeh backend."""
24
- self._marker_map = {
25
- "o": "circle",
26
- "D": "diamond",
27
- "s": "square",
28
- "^": "triangle",
29
- "v": "inverted_triangle",
30
- }
39
+ pass
31
40
 
32
41
  def create_figure(
33
42
  self,
@@ -131,7 +140,7 @@ class BokehBackend:
131
140
  source = ColumnDataSource(data)
132
141
 
133
142
  # Get marker type for scatter()
134
- marker_type = self._marker_map.get(marker, "circle")
143
+ marker_type = self._MARKER_MAP.get(marker, "circle")
135
144
 
136
145
  # Create scatter using scatter() method (Bokeh 3.4+ preferred API)
137
146
  scatter_kwargs = {
@@ -171,14 +180,7 @@ class BokehBackend:
171
180
  label: Optional[str] = None,
172
181
  ) -> Any:
173
182
  """Create a line plot on the given figure."""
174
- # Convert linestyle
175
- dash_map = {
176
- "-": "solid",
177
- "--": "dashed",
178
- ":": "dotted",
179
- "-.": "dashdot",
180
- }
181
- line_dash = dash_map.get(linestyle, "solid")
183
+ line_dash = self._DASH_MAP.get(linestyle, "solid")
182
184
 
183
185
  line_kwargs = {
184
186
  "line_color": color,
@@ -233,8 +235,7 @@ class BokehBackend:
233
235
  zorder: int = 1,
234
236
  ) -> Any:
235
237
  """Add a horizontal line across the figure."""
236
- dash_map = {"-": "solid", "--": "dashed", ":": "dotted", "-.": "dashdot"}
237
- line_dash = dash_map.get(linestyle, "dashed")
238
+ line_dash = self._DASH_MAP.get(linestyle, "dashed")
238
239
 
239
240
  span = Span(
240
241
  location=y,
@@ -411,8 +412,7 @@ class BokehBackend:
411
412
  yaxis_name: str = "secondary",
412
413
  ) -> Any:
413
414
  """Create a line plot on secondary y-axis."""
414
- dash_map = {"-": "solid", "--": "dashed", ":": "dotted", "-.": "dashdot"}
415
- line_dash = dash_map.get(linestyle, "solid")
415
+ line_dash = self._DASH_MAP.get(linestyle, "solid")
416
416
 
417
417
  return ax.line(
418
418
  x.values,
@@ -683,6 +683,102 @@ class BokehBackend:
683
683
  ax.legend.background_fill_alpha = 0.9
684
684
  ax.legend.border_line_color = "black"
685
685
 
686
+ def axvline(
687
+ self,
688
+ ax: figure,
689
+ x: float,
690
+ color: str = "grey",
691
+ linestyle: str = "--",
692
+ linewidth: float = 1.0,
693
+ alpha: float = 1.0,
694
+ zorder: int = 1,
695
+ ) -> Any:
696
+ """Add a vertical line across the figure."""
697
+ line_dash = self._DASH_MAP.get(linestyle, "dashed")
698
+
699
+ span = Span(
700
+ location=x,
701
+ dimension="height",
702
+ line_color=color,
703
+ line_dash=line_dash,
704
+ line_width=linewidth,
705
+ line_alpha=alpha,
706
+ )
707
+ ax.add_layout(span)
708
+ return span
709
+
710
+ def hbar(
711
+ self,
712
+ ax: figure,
713
+ y: pd.Series,
714
+ width: pd.Series,
715
+ height: float = 0.8,
716
+ left: Union[float, pd.Series] = 0,
717
+ color: Union[str, List[str]] = "blue",
718
+ edgecolor: str = "black",
719
+ linewidth: float = 0.5,
720
+ zorder: int = 2,
721
+ ) -> Any:
722
+ """Create horizontal bar chart."""
723
+ # Convert left to array if scalar
724
+ if isinstance(left, (int, float)):
725
+ left_arr = [left] * len(y)
726
+ else:
727
+ left_arr = list(left) if hasattr(left, "tolist") else left
728
+
729
+ # Calculate right edge
730
+ right_arr = [left_val + w for left_val, w in zip(left_arr, width)]
731
+
732
+ return ax.hbar(
733
+ y=y.values,
734
+ right=right_arr,
735
+ left=left_arr,
736
+ height=height,
737
+ fill_color=color,
738
+ line_color=edgecolor,
739
+ line_width=linewidth,
740
+ )
741
+
742
+ def errorbar_h(
743
+ self,
744
+ ax: figure,
745
+ x: pd.Series,
746
+ y: pd.Series,
747
+ xerr_lower: pd.Series,
748
+ xerr_upper: pd.Series,
749
+ color: str = "black",
750
+ linewidth: float = 1.5,
751
+ capsize: float = 3,
752
+ zorder: int = 3,
753
+ ) -> Any:
754
+ """Add horizontal error bars."""
755
+ from bokeh.models import Whisker
756
+
757
+ # Calculate bounds
758
+ lower = x - xerr_lower
759
+ upper = x + xerr_upper
760
+
761
+ source = ColumnDataSource(
762
+ data={
763
+ "y": y.values,
764
+ "lower": lower.values,
765
+ "upper": upper.values,
766
+ }
767
+ )
768
+
769
+ # Add horizontal whisker
770
+ whisker = Whisker(
771
+ source=source,
772
+ base="y",
773
+ lower="lower",
774
+ upper="upper",
775
+ dimension="width",
776
+ line_color=color,
777
+ line_width=linewidth,
778
+ )
779
+ ax.add_layout(whisker)
780
+ return whisker
781
+
686
782
  def finalize_layout(
687
783
  self,
688
784
  fig: Any,
@@ -394,6 +394,75 @@ class MatplotlibBackend:
394
394
  """Add simple legend for labeled scatter data."""
395
395
  ax.legend(loc=loc, fontsize=9)
396
396
 
397
+ def axvline(
398
+ self,
399
+ ax: Axes,
400
+ x: float,
401
+ color: str = "grey",
402
+ linestyle: str = "--",
403
+ linewidth: float = 1.0,
404
+ alpha: float = 1.0,
405
+ zorder: int = 1,
406
+ ) -> Any:
407
+ """Add a vertical line across the axes."""
408
+ return ax.axvline(
409
+ x=x,
410
+ color=color,
411
+ linestyle=linestyle,
412
+ linewidth=linewidth,
413
+ alpha=alpha,
414
+ zorder=zorder,
415
+ )
416
+
417
+ def hbar(
418
+ self,
419
+ ax: Axes,
420
+ y: pd.Series,
421
+ width: pd.Series,
422
+ height: float = 0.8,
423
+ left: Union[float, pd.Series] = 0,
424
+ color: Union[str, List[str]] = "blue",
425
+ edgecolor: str = "black",
426
+ linewidth: float = 0.5,
427
+ zorder: int = 2,
428
+ ) -> Any:
429
+ """Create horizontal bar chart."""
430
+ return ax.barh(
431
+ y=y,
432
+ width=width,
433
+ height=height,
434
+ left=left,
435
+ color=color,
436
+ edgecolor=edgecolor,
437
+ linewidth=linewidth,
438
+ zorder=zorder,
439
+ )
440
+
441
+ def errorbar_h(
442
+ self,
443
+ ax: Axes,
444
+ x: pd.Series,
445
+ y: pd.Series,
446
+ xerr_lower: pd.Series,
447
+ xerr_upper: pd.Series,
448
+ color: str = "black",
449
+ linewidth: float = 1.5,
450
+ capsize: float = 3,
451
+ zorder: int = 3,
452
+ ) -> Any:
453
+ """Add horizontal error bars."""
454
+ xerr = [xerr_lower.values, xerr_upper.values]
455
+ return ax.errorbar(
456
+ x=x,
457
+ y=y,
458
+ xerr=xerr,
459
+ fmt="none",
460
+ ecolor=color,
461
+ elinewidth=linewidth,
462
+ capsize=capsize,
463
+ zorder=zorder,
464
+ )
465
+
397
466
  def finalize_layout(
398
467
  self,
399
468
  fig: Figure,
@@ -20,15 +20,24 @@ class PlotlyBackend:
20
20
  - Nearest gene
21
21
  """
22
22
 
23
+ # Class constants for style mappings
24
+ _MARKER_SYMBOLS = {
25
+ "o": "circle",
26
+ "D": "diamond",
27
+ "s": "square",
28
+ "^": "triangle-up",
29
+ "v": "triangle-down",
30
+ }
31
+ _DASH_MAP = {
32
+ "-": "solid",
33
+ "--": "dash",
34
+ ":": "dot",
35
+ "-.": "dashdot",
36
+ }
37
+
23
38
  def __init__(self) -> None:
24
39
  """Initialize the plotly backend."""
25
- self._marker_symbols = {
26
- "o": "circle",
27
- "D": "diamond",
28
- "s": "square",
29
- "^": "triangle-up",
30
- "v": "triangle-down",
31
- }
40
+ pass
32
41
 
33
42
  def create_figure(
34
43
  self,
@@ -111,7 +120,7 @@ class PlotlyBackend:
111
120
  fig, row = ax
112
121
 
113
122
  # Convert matplotlib marker to plotly symbol
114
- symbol = self._marker_symbols.get(marker, "circle")
123
+ symbol = self._MARKER_SYMBOLS.get(marker, "circle")
115
124
 
116
125
  # Convert size (matplotlib uses area, plotly uses diameter)
117
126
  if isinstance(sizes, (int, float)):
@@ -178,15 +187,7 @@ class PlotlyBackend:
178
187
  ) -> Any:
179
188
  """Create a line plot on the given panel."""
180
189
  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")
190
+ dash = self._DASH_MAP.get(linestyle, "solid")
190
191
 
191
192
  trace = go.Scatter(
192
193
  x=x,
@@ -244,9 +245,7 @@ class PlotlyBackend:
244
245
  ) -> Any:
245
246
  """Add a horizontal line across the panel."""
246
247
  fig, row = ax
247
-
248
- dash_map = {"-": "solid", "--": "dash", ":": "dot", "-.": "dashdot"}
249
- dash = dash_map.get(linestyle, "dash")
248
+ dash = self._DASH_MAP.get(linestyle, "dash")
250
249
 
251
250
  fig.add_hline(
252
251
  y=y,
@@ -461,9 +460,7 @@ class PlotlyBackend:
461
460
  ) -> Any:
462
461
  """Create a line plot on secondary y-axis."""
463
462
  fig, row = ax
464
-
465
- dash_map = {"-": "solid", "--": "dash", ":": "dot", "-.": "dashdot"}
466
- dash = dash_map.get(linestyle, "solid")
463
+ dash = self._DASH_MAP.get(linestyle, "solid")
467
464
 
468
465
  trace = go.Scatter(
469
466
  x=x,
@@ -782,6 +779,101 @@ class PlotlyBackend:
782
779
  )
783
780
  )
784
781
 
782
+ def axvline(
783
+ self,
784
+ ax: Tuple[go.Figure, int],
785
+ x: float,
786
+ color: str = "grey",
787
+ linestyle: str = "--",
788
+ linewidth: float = 1.0,
789
+ alpha: float = 1.0,
790
+ zorder: int = 1,
791
+ ) -> Any:
792
+ """Add a vertical line across the panel."""
793
+ fig, row = ax
794
+ dash = self._DASH_MAP.get(linestyle, "dash")
795
+
796
+ fig.add_vline(
797
+ x=x,
798
+ line_dash=dash,
799
+ line_color=color,
800
+ line_width=linewidth,
801
+ opacity=alpha,
802
+ row=row,
803
+ col=1,
804
+ )
805
+
806
+ def hbar(
807
+ self,
808
+ ax: Tuple[go.Figure, int],
809
+ y: pd.Series,
810
+ width: pd.Series,
811
+ height: float = 0.8,
812
+ left: Union[float, pd.Series] = 0,
813
+ color: Union[str, List[str]] = "blue",
814
+ edgecolor: str = "black",
815
+ linewidth: float = 0.5,
816
+ zorder: int = 2,
817
+ ) -> Any:
818
+ """Create horizontal bar chart."""
819
+ fig, row = ax
820
+
821
+ # Convert left to array if scalar
822
+ if isinstance(left, (int, float)):
823
+ left_arr = [left] * len(y)
824
+ else:
825
+ left_arr = list(left) if hasattr(left, "tolist") else left
826
+
827
+ trace = go.Bar(
828
+ y=y,
829
+ x=width,
830
+ orientation="h",
831
+ base=left_arr,
832
+ marker=dict(
833
+ color=color,
834
+ line=dict(color=edgecolor, width=linewidth),
835
+ ),
836
+ showlegend=False,
837
+ )
838
+
839
+ fig.add_trace(trace, row=row, col=1)
840
+ return trace
841
+
842
+ def errorbar_h(
843
+ self,
844
+ ax: Tuple[go.Figure, int],
845
+ x: pd.Series,
846
+ y: pd.Series,
847
+ xerr_lower: pd.Series,
848
+ xerr_upper: pd.Series,
849
+ color: str = "black",
850
+ linewidth: float = 1.5,
851
+ capsize: float = 3,
852
+ zorder: int = 3,
853
+ ) -> Any:
854
+ """Add horizontal error bars."""
855
+ fig, row = ax
856
+
857
+ trace = go.Scatter(
858
+ x=x,
859
+ y=y,
860
+ mode="markers",
861
+ marker=dict(size=0),
862
+ error_x=dict(
863
+ type="data",
864
+ symmetric=False,
865
+ array=xerr_upper,
866
+ arrayminus=xerr_lower,
867
+ color=color,
868
+ thickness=linewidth,
869
+ width=capsize,
870
+ ),
871
+ showlegend=False,
872
+ )
873
+
874
+ fig.add_trace(trace, row=row, col=1)
875
+ return trace
876
+
785
877
  def finalize_layout(
786
878
  self,
787
879
  fig: go.Figure,
pylocuszoom/colors.py CHANGED
@@ -239,3 +239,44 @@ def get_credible_set_color_palette(n_sets: int = 10) -> dict[int, str]:
239
239
  return {
240
240
  i + 1: CREDIBLE_SET_COLORS[i % len(CREDIBLE_SET_COLORS)] for i in range(n_sets)
241
241
  }
242
+
243
+
244
+ # PheWAS category colors - distinct colors for phenotype categories
245
+ PHEWAS_CATEGORY_COLORS: List[str] = [
246
+ "#E41A1C", # red
247
+ "#377EB8", # blue
248
+ "#4DAF4A", # green
249
+ "#984EA3", # purple
250
+ "#FF7F00", # orange
251
+ "#FFFF33", # yellow
252
+ "#A65628", # brown
253
+ "#F781BF", # pink
254
+ "#999999", # grey
255
+ "#66C2A5", # teal
256
+ "#FC8D62", # salmon
257
+ "#8DA0CB", # periwinkle
258
+ ]
259
+
260
+
261
+ def get_phewas_category_color(category_idx: int) -> str:
262
+ """Get color for a PheWAS category by index.
263
+
264
+ Args:
265
+ category_idx: Zero-indexed category number.
266
+
267
+ Returns:
268
+ Hex color code string.
269
+ """
270
+ return PHEWAS_CATEGORY_COLORS[category_idx % len(PHEWAS_CATEGORY_COLORS)]
271
+
272
+
273
+ def get_phewas_category_palette(categories: List[str]) -> dict[str, str]:
274
+ """Get color palette mapping category names to colors.
275
+
276
+ Args:
277
+ categories: List of unique category names.
278
+
279
+ Returns:
280
+ Dictionary mapping category names to hex colors.
281
+ """
282
+ return {cat: get_phewas_category_color(i) for i, cat in enumerate(categories)}
pylocuszoom/forest.py ADDED
@@ -0,0 +1,37 @@
1
+ """Forest plot data validation and preparation.
2
+
3
+ Validates and prepares meta-analysis/forest plot data for visualization.
4
+ """
5
+
6
+ import pandas as pd
7
+
8
+ from .utils import ValidationError
9
+
10
+
11
+ def validate_forest_df(
12
+ df: pd.DataFrame,
13
+ study_col: str = "study",
14
+ effect_col: str = "effect",
15
+ ci_lower_col: str = "ci_lower",
16
+ ci_upper_col: str = "ci_upper",
17
+ ) -> None:
18
+ """Validate forest plot DataFrame has required columns.
19
+
20
+ Args:
21
+ df: Forest plot data DataFrame.
22
+ study_col: Column name for study/phenotype names.
23
+ effect_col: Column name for effect sizes (beta, OR, HR).
24
+ ci_lower_col: Column name for lower confidence interval.
25
+ ci_upper_col: Column name for upper confidence interval.
26
+
27
+ Raises:
28
+ ValidationError: If required columns are missing.
29
+ """
30
+ required = [study_col, effect_col, ci_lower_col, ci_upper_col]
31
+ missing = [col for col in required if col not in df.columns]
32
+
33
+ if missing:
34
+ raise ValidationError(
35
+ f"Forest plot DataFrame missing required columns: {missing}. "
36
+ f"Required: {required}. Found: {list(df.columns)}"
37
+ )