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
|
@@ -11,7 +11,10 @@ from bokeh.layouts import column
|
|
|
11
11
|
from bokeh.models import ColumnDataSource, DataRange1d, HoverTool, Span
|
|
12
12
|
from bokeh.plotting import figure
|
|
13
13
|
|
|
14
|
+
from . import convert_latex_to_unicode, register_backend
|
|
14
15
|
|
|
16
|
+
|
|
17
|
+
@register_backend("bokeh")
|
|
15
18
|
class BokehBackend:
|
|
16
19
|
"""Bokeh backend for interactive plot generation.
|
|
17
20
|
|
|
@@ -19,15 +22,35 @@ class BokehBackend:
|
|
|
19
22
|
applications and dashboards.
|
|
20
23
|
"""
|
|
21
24
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
25
|
+
# Class constants for style mappings
|
|
26
|
+
_MARKER_MAP = {
|
|
27
|
+
"o": "circle",
|
|
28
|
+
"D": "diamond",
|
|
29
|
+
"s": "square",
|
|
30
|
+
"^": "triangle",
|
|
31
|
+
"v": "inverted_triangle",
|
|
32
|
+
}
|
|
33
|
+
_DASH_MAP = {
|
|
34
|
+
"-": "solid",
|
|
35
|
+
"--": "dashed",
|
|
36
|
+
":": "dotted",
|
|
37
|
+
"-.": "dashdot",
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
@property
|
|
41
|
+
def supports_snp_labels(self) -> bool:
|
|
42
|
+
"""Bokeh uses hover tooltips instead of labels."""
|
|
43
|
+
return False
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
def supports_hover(self) -> bool:
|
|
47
|
+
"""Bokeh supports hover tooltips."""
|
|
48
|
+
return True
|
|
49
|
+
|
|
50
|
+
@property
|
|
51
|
+
def supports_secondary_axis(self) -> bool:
|
|
52
|
+
"""Bokeh supports secondary y-axis."""
|
|
53
|
+
return True
|
|
31
54
|
|
|
32
55
|
def create_figure(
|
|
33
56
|
self,
|
|
@@ -131,7 +154,7 @@ class BokehBackend:
|
|
|
131
154
|
source = ColumnDataSource(data)
|
|
132
155
|
|
|
133
156
|
# Get marker type for scatter()
|
|
134
|
-
marker_type = self.
|
|
157
|
+
marker_type = self._MARKER_MAP.get(marker, "circle")
|
|
135
158
|
|
|
136
159
|
# Create scatter using scatter() method (Bokeh 3.4+ preferred API)
|
|
137
160
|
scatter_kwargs = {
|
|
@@ -171,14 +194,7 @@ class BokehBackend:
|
|
|
171
194
|
label: Optional[str] = None,
|
|
172
195
|
) -> Any:
|
|
173
196
|
"""Create a line plot on the given figure."""
|
|
174
|
-
|
|
175
|
-
dash_map = {
|
|
176
|
-
"-": "solid",
|
|
177
|
-
"--": "dashed",
|
|
178
|
-
":": "dotted",
|
|
179
|
-
"-.": "dashdot",
|
|
180
|
-
}
|
|
181
|
-
line_dash = dash_map.get(linestyle, "solid")
|
|
197
|
+
line_dash = self._DASH_MAP.get(linestyle, "solid")
|
|
182
198
|
|
|
183
199
|
line_kwargs = {
|
|
184
200
|
"line_color": color,
|
|
@@ -233,8 +249,7 @@ class BokehBackend:
|
|
|
233
249
|
zorder: int = 1,
|
|
234
250
|
) -> Any:
|
|
235
251
|
"""Add a horizontal line across the figure."""
|
|
236
|
-
|
|
237
|
-
line_dash = dash_map.get(linestyle, "dashed")
|
|
252
|
+
line_dash = self._DASH_MAP.get(linestyle, "dashed")
|
|
238
253
|
|
|
239
254
|
span = Span(
|
|
240
255
|
location=y,
|
|
@@ -355,6 +370,18 @@ class BokehBackend:
|
|
|
355
370
|
ax.yaxis.axis_label = label
|
|
356
371
|
ax.yaxis.axis_label_text_font_size = f"{fontsize}pt"
|
|
357
372
|
|
|
373
|
+
def set_yticks(
|
|
374
|
+
self,
|
|
375
|
+
ax: figure,
|
|
376
|
+
positions: List[float],
|
|
377
|
+
labels: List[str],
|
|
378
|
+
fontsize: int = 10,
|
|
379
|
+
) -> None:
|
|
380
|
+
"""Set y-axis tick positions and labels."""
|
|
381
|
+
ax.yaxis.ticker = positions
|
|
382
|
+
ax.yaxis.major_label_overrides = dict(zip(positions, labels))
|
|
383
|
+
ax.yaxis.major_label_text_font_size = f"{fontsize}pt"
|
|
384
|
+
|
|
358
385
|
def _get_legend_location(self, loc: str, default: str = "top_left") -> str:
|
|
359
386
|
"""Map matplotlib-style legend location to Bokeh location."""
|
|
360
387
|
loc_map = {
|
|
@@ -367,18 +394,7 @@ class BokehBackend:
|
|
|
367
394
|
|
|
368
395
|
def _convert_label(self, label: str) -> str:
|
|
369
396
|
"""Convert LaTeX-style labels to Unicode for Bokeh display."""
|
|
370
|
-
|
|
371
|
-
(r"$-\log_{10}$ P", "-log₁₀(P)"),
|
|
372
|
-
(r"$-\log_{10}$", "-log₁₀"),
|
|
373
|
-
(r"\log_{10}", "log₁₀"),
|
|
374
|
-
(r"$r^2$", "r²"),
|
|
375
|
-
(r"$R^2$", "R²"),
|
|
376
|
-
]
|
|
377
|
-
for latex, unicode_str in conversions:
|
|
378
|
-
if latex in label:
|
|
379
|
-
label = label.replace(latex, unicode_str)
|
|
380
|
-
label = label.replace("$", "")
|
|
381
|
-
return label
|
|
397
|
+
return convert_latex_to_unicode(label)
|
|
382
398
|
|
|
383
399
|
def set_title(self, ax: figure, title: str, fontsize: int = 14) -> None:
|
|
384
400
|
"""Set figure title."""
|
|
@@ -411,8 +427,7 @@ class BokehBackend:
|
|
|
411
427
|
yaxis_name: str = "secondary",
|
|
412
428
|
) -> Any:
|
|
413
429
|
"""Create a line plot on secondary y-axis."""
|
|
414
|
-
|
|
415
|
-
line_dash = dash_map.get(linestyle, "solid")
|
|
430
|
+
line_dash = self._DASH_MAP.get(linestyle, "solid")
|
|
416
431
|
|
|
417
432
|
return ax.line(
|
|
418
433
|
x.values,
|
|
@@ -489,6 +504,53 @@ class BokehBackend:
|
|
|
489
504
|
renderer.major_label_text_color = color
|
|
490
505
|
break
|
|
491
506
|
|
|
507
|
+
def add_snp_labels(
|
|
508
|
+
self,
|
|
509
|
+
ax: figure,
|
|
510
|
+
df: pd.DataFrame,
|
|
511
|
+
pos_col: str,
|
|
512
|
+
neglog10p_col: str,
|
|
513
|
+
rs_col: str,
|
|
514
|
+
label_top_n: int,
|
|
515
|
+
genes_df: Optional[pd.DataFrame],
|
|
516
|
+
chrom: int,
|
|
517
|
+
) -> None:
|
|
518
|
+
"""No-op: Bokeh uses hover tooltips instead of text labels."""
|
|
519
|
+
pass
|
|
520
|
+
|
|
521
|
+
def add_panel_label(
|
|
522
|
+
self,
|
|
523
|
+
ax: figure,
|
|
524
|
+
label: str,
|
|
525
|
+
x_frac: float = 0.02,
|
|
526
|
+
y_frac: float = 0.95,
|
|
527
|
+
) -> None:
|
|
528
|
+
"""Add label text at fractional position in panel."""
|
|
529
|
+
from bokeh.models import Label
|
|
530
|
+
|
|
531
|
+
# Convert fraction to data coordinates using axis ranges
|
|
532
|
+
x_range = ax.x_range
|
|
533
|
+
y_range = ax.y_range
|
|
534
|
+
x = (
|
|
535
|
+
x_range.start + x_frac * (x_range.end - x_range.start)
|
|
536
|
+
if hasattr(x_range, "start") and x_range.start is not None
|
|
537
|
+
else 0
|
|
538
|
+
)
|
|
539
|
+
y = (
|
|
540
|
+
y_range.start + y_frac * (y_range.end - y_range.start)
|
|
541
|
+
if hasattr(y_range, "start") and y_range.start is not None
|
|
542
|
+
else 0
|
|
543
|
+
)
|
|
544
|
+
|
|
545
|
+
label_obj = Label(
|
|
546
|
+
x=x,
|
|
547
|
+
y=y,
|
|
548
|
+
text=label,
|
|
549
|
+
text_font_size="12px",
|
|
550
|
+
text_font_style="bold",
|
|
551
|
+
)
|
|
552
|
+
ax.add_layout(label_obj)
|
|
553
|
+
|
|
492
554
|
def _ensure_legend_range(self, ax: figure) -> Any:
|
|
493
555
|
"""Ensure legend range exists and return a dummy data source.
|
|
494
556
|
|
|
@@ -683,6 +745,102 @@ class BokehBackend:
|
|
|
683
745
|
ax.legend.background_fill_alpha = 0.9
|
|
684
746
|
ax.legend.border_line_color = "black"
|
|
685
747
|
|
|
748
|
+
def axvline(
|
|
749
|
+
self,
|
|
750
|
+
ax: figure,
|
|
751
|
+
x: float,
|
|
752
|
+
color: str = "grey",
|
|
753
|
+
linestyle: str = "--",
|
|
754
|
+
linewidth: float = 1.0,
|
|
755
|
+
alpha: float = 1.0,
|
|
756
|
+
zorder: int = 1,
|
|
757
|
+
) -> Any:
|
|
758
|
+
"""Add a vertical line across the figure."""
|
|
759
|
+
line_dash = self._DASH_MAP.get(linestyle, "dashed")
|
|
760
|
+
|
|
761
|
+
span = Span(
|
|
762
|
+
location=x,
|
|
763
|
+
dimension="height",
|
|
764
|
+
line_color=color,
|
|
765
|
+
line_dash=line_dash,
|
|
766
|
+
line_width=linewidth,
|
|
767
|
+
line_alpha=alpha,
|
|
768
|
+
)
|
|
769
|
+
ax.add_layout(span)
|
|
770
|
+
return span
|
|
771
|
+
|
|
772
|
+
def hbar(
|
|
773
|
+
self,
|
|
774
|
+
ax: figure,
|
|
775
|
+
y: pd.Series,
|
|
776
|
+
width: pd.Series,
|
|
777
|
+
height: float = 0.8,
|
|
778
|
+
left: Union[float, pd.Series] = 0,
|
|
779
|
+
color: Union[str, List[str]] = "blue",
|
|
780
|
+
edgecolor: str = "black",
|
|
781
|
+
linewidth: float = 0.5,
|
|
782
|
+
zorder: int = 2,
|
|
783
|
+
) -> Any:
|
|
784
|
+
"""Create horizontal bar chart."""
|
|
785
|
+
# Convert left to array if scalar
|
|
786
|
+
if isinstance(left, (int, float)):
|
|
787
|
+
left_arr = [left] * len(y)
|
|
788
|
+
else:
|
|
789
|
+
left_arr = list(left) if hasattr(left, "tolist") else left
|
|
790
|
+
|
|
791
|
+
# Calculate right edge
|
|
792
|
+
right_arr = [left_val + w for left_val, w in zip(left_arr, width)]
|
|
793
|
+
|
|
794
|
+
return ax.hbar(
|
|
795
|
+
y=y.values,
|
|
796
|
+
right=right_arr,
|
|
797
|
+
left=left_arr,
|
|
798
|
+
height=height,
|
|
799
|
+
fill_color=color,
|
|
800
|
+
line_color=edgecolor,
|
|
801
|
+
line_width=linewidth,
|
|
802
|
+
)
|
|
803
|
+
|
|
804
|
+
def errorbar_h(
|
|
805
|
+
self,
|
|
806
|
+
ax: figure,
|
|
807
|
+
x: pd.Series,
|
|
808
|
+
y: pd.Series,
|
|
809
|
+
xerr_lower: pd.Series,
|
|
810
|
+
xerr_upper: pd.Series,
|
|
811
|
+
color: str = "black",
|
|
812
|
+
linewidth: float = 1.5,
|
|
813
|
+
capsize: float = 3,
|
|
814
|
+
zorder: int = 3,
|
|
815
|
+
) -> Any:
|
|
816
|
+
"""Add horizontal error bars."""
|
|
817
|
+
from bokeh.models import Whisker
|
|
818
|
+
|
|
819
|
+
# Calculate bounds
|
|
820
|
+
lower = x - xerr_lower
|
|
821
|
+
upper = x + xerr_upper
|
|
822
|
+
|
|
823
|
+
source = ColumnDataSource(
|
|
824
|
+
data={
|
|
825
|
+
"y": y.values,
|
|
826
|
+
"lower": lower.values,
|
|
827
|
+
"upper": upper.values,
|
|
828
|
+
}
|
|
829
|
+
)
|
|
830
|
+
|
|
831
|
+
# Add horizontal whisker
|
|
832
|
+
whisker = Whisker(
|
|
833
|
+
source=source,
|
|
834
|
+
base="y",
|
|
835
|
+
lower="lower",
|
|
836
|
+
upper="upper",
|
|
837
|
+
dimension="width",
|
|
838
|
+
line_color=color,
|
|
839
|
+
line_width=linewidth,
|
|
840
|
+
)
|
|
841
|
+
ax.add_layout(whisker)
|
|
842
|
+
return whisker
|
|
843
|
+
|
|
686
844
|
def finalize_layout(
|
|
687
845
|
self,
|
|
688
846
|
fig: Any,
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
"""Hover data builder for consistent hover tooltip generation.
|
|
2
|
+
|
|
3
|
+
Provides a unified interface for building hover data across Plotly and Bokeh backends.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
from typing import Optional
|
|
8
|
+
|
|
9
|
+
import pandas as pd
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass
|
|
13
|
+
class HoverConfig:
|
|
14
|
+
"""Configuration for hover data column mapping.
|
|
15
|
+
|
|
16
|
+
Maps source DataFrame column names to standardized display names for tooltips.
|
|
17
|
+
|
|
18
|
+
Attributes:
|
|
19
|
+
snp_col: Column name for SNP identifiers (displayed as "SNP").
|
|
20
|
+
pos_col: Column name for genomic position (displayed as "Position").
|
|
21
|
+
p_col: Column name for p-value (displayed as "P-value").
|
|
22
|
+
ld_col: Column name for LD/R-squared (displayed as "R²").
|
|
23
|
+
extra_cols: Additional columns to include, mapping source name to display name.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
snp_col: Optional[str] = None
|
|
27
|
+
pos_col: Optional[str] = None
|
|
28
|
+
p_col: Optional[str] = None
|
|
29
|
+
ld_col: Optional[str] = None
|
|
30
|
+
extra_cols: dict[str, str] = field(default_factory=dict)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class HoverDataBuilder:
|
|
34
|
+
"""Builder for constructing hover data and templates across backends.
|
|
35
|
+
|
|
36
|
+
Provides consistent hover tooltip generation for Plotly and Bokeh backends
|
|
37
|
+
with automatic format detection for common column types.
|
|
38
|
+
|
|
39
|
+
Attributes:
|
|
40
|
+
FORMAT_SPECS: Format specifiers for different data types.
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
FORMAT_SPECS = {
|
|
44
|
+
"p_value": ".2e",
|
|
45
|
+
"r2": ".3f",
|
|
46
|
+
"position": ",.0f",
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
# Standard column mappings (source config attr -> display name)
|
|
50
|
+
_COLUMN_MAPPING = {
|
|
51
|
+
"snp_col": "SNP",
|
|
52
|
+
"pos_col": "Position",
|
|
53
|
+
"p_col": "P-value",
|
|
54
|
+
"ld_col": "R²",
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
def __init__(self, config: HoverConfig) -> None:
|
|
58
|
+
"""Initialize builder with column configuration.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
config: HoverConfig with column name mappings.
|
|
62
|
+
"""
|
|
63
|
+
self.config = config
|
|
64
|
+
|
|
65
|
+
def build_dataframe(self, df: pd.DataFrame) -> Optional[pd.DataFrame]:
|
|
66
|
+
"""Build standardized hover DataFrame with renamed columns.
|
|
67
|
+
|
|
68
|
+
Extracts configured columns from the input DataFrame, renames them to
|
|
69
|
+
standardized display names, and returns a new DataFrame. Columns that
|
|
70
|
+
don't exist in the input are skipped gracefully.
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
df: Input DataFrame containing hover data columns.
|
|
74
|
+
|
|
75
|
+
Returns:
|
|
76
|
+
DataFrame with renamed columns, or None if no configured columns exist.
|
|
77
|
+
"""
|
|
78
|
+
result_data = {}
|
|
79
|
+
|
|
80
|
+
# Process standard columns in order
|
|
81
|
+
for config_attr, display_name in self._COLUMN_MAPPING.items():
|
|
82
|
+
source_col = getattr(self.config, config_attr)
|
|
83
|
+
if source_col is not None and source_col in df.columns:
|
|
84
|
+
result_data[display_name] = df[source_col].values
|
|
85
|
+
|
|
86
|
+
# Process extra columns
|
|
87
|
+
for source_col, display_name in self.config.extra_cols.items():
|
|
88
|
+
if source_col in df.columns:
|
|
89
|
+
result_data[display_name] = df[source_col].values
|
|
90
|
+
|
|
91
|
+
if not result_data:
|
|
92
|
+
return None
|
|
93
|
+
|
|
94
|
+
return pd.DataFrame(result_data)
|
|
95
|
+
|
|
96
|
+
def build_plotly_template(self, hover_df: pd.DataFrame) -> str:
|
|
97
|
+
"""Generate Plotly hovertemplate string.
|
|
98
|
+
|
|
99
|
+
Creates a template string with appropriate format specifiers for each
|
|
100
|
+
column type. SNP appears first in bold, followed by other columns with
|
|
101
|
+
their values.
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
hover_df: DataFrame returned by build_dataframe().
|
|
105
|
+
|
|
106
|
+
Returns:
|
|
107
|
+
Plotly hovertemplate string with customdata references.
|
|
108
|
+
"""
|
|
109
|
+
columns = hover_df.columns.tolist()
|
|
110
|
+
parts = []
|
|
111
|
+
|
|
112
|
+
for i, col in enumerate(columns):
|
|
113
|
+
if i == 0:
|
|
114
|
+
# First column (SNP) in bold
|
|
115
|
+
parts.append(f"<b>%{{customdata[{i}]}}</b>")
|
|
116
|
+
else:
|
|
117
|
+
fmt = self._detect_plotly_format(col)
|
|
118
|
+
if fmt:
|
|
119
|
+
parts.append(f"{col}: %{{customdata[{i}]:{fmt}}}")
|
|
120
|
+
else:
|
|
121
|
+
parts.append(f"{col}: %{{customdata[{i}]}}")
|
|
122
|
+
|
|
123
|
+
parts.append("<extra></extra>")
|
|
124
|
+
return "<br>".join(parts)
|
|
125
|
+
|
|
126
|
+
def build_bokeh_tooltips(self, hover_df: pd.DataFrame) -> list[tuple[str, str]]:
|
|
127
|
+
"""Generate Bokeh tooltips list.
|
|
128
|
+
|
|
129
|
+
Creates a list of (name, format) tuples for Bokeh HoverTool configuration.
|
|
130
|
+
Each tuple contains the display name and the Bokeh format string with
|
|
131
|
+
appropriate specifiers.
|
|
132
|
+
|
|
133
|
+
Args:
|
|
134
|
+
hover_df: DataFrame returned by build_dataframe().
|
|
135
|
+
|
|
136
|
+
Returns:
|
|
137
|
+
List of (name, format_string) tuples for Bokeh tooltips.
|
|
138
|
+
"""
|
|
139
|
+
tooltips = []
|
|
140
|
+
|
|
141
|
+
for col in hover_df.columns:
|
|
142
|
+
fmt = self._detect_bokeh_format(col)
|
|
143
|
+
if fmt:
|
|
144
|
+
tooltips.append((col, f"@{{{col}}}{{{fmt}}}"))
|
|
145
|
+
else:
|
|
146
|
+
tooltips.append((col, f"@{col}"))
|
|
147
|
+
|
|
148
|
+
return tooltips
|
|
149
|
+
|
|
150
|
+
def _detect_plotly_format(self, col_name: str) -> Optional[str]:
|
|
151
|
+
"""Detect appropriate Plotly format specifier for column.
|
|
152
|
+
|
|
153
|
+
Uses heuristics based on column name to determine formatting:
|
|
154
|
+
- P-value columns: scientific notation (.2e)
|
|
155
|
+
- R²/LD columns: 3 decimal places (.3f)
|
|
156
|
+
- Position columns: comma-separated (.0f)
|
|
157
|
+
|
|
158
|
+
Args:
|
|
159
|
+
col_name: Display name of the column.
|
|
160
|
+
|
|
161
|
+
Returns:
|
|
162
|
+
Plotly format specifier string, or None for default format.
|
|
163
|
+
"""
|
|
164
|
+
col_lower = col_name.lower()
|
|
165
|
+
|
|
166
|
+
if col_lower in ("p-value", "pval", "p_value"):
|
|
167
|
+
return self.FORMAT_SPECS["p_value"]
|
|
168
|
+
elif any(x in col_lower for x in ("r2", "r²", "ld")):
|
|
169
|
+
return self.FORMAT_SPECS["r2"]
|
|
170
|
+
elif "pos" in col_lower:
|
|
171
|
+
return self.FORMAT_SPECS["position"]
|
|
172
|
+
|
|
173
|
+
return None
|
|
174
|
+
|
|
175
|
+
def _detect_bokeh_format(self, col_name: str) -> Optional[str]:
|
|
176
|
+
"""Detect appropriate Bokeh format code for column.
|
|
177
|
+
|
|
178
|
+
Uses heuristics based on column name to determine formatting:
|
|
179
|
+
- P-value columns: scientific notation (0.2e)
|
|
180
|
+
- R²/LD columns: 3 decimal places (0.3f)
|
|
181
|
+
- Position columns: comma-separated (0,0)
|
|
182
|
+
|
|
183
|
+
Args:
|
|
184
|
+
col_name: Display name of the column.
|
|
185
|
+
|
|
186
|
+
Returns:
|
|
187
|
+
Bokeh format code string, or None for default format.
|
|
188
|
+
"""
|
|
189
|
+
col_lower = col_name.lower()
|
|
190
|
+
|
|
191
|
+
if col_lower in ("p-value", "pval", "p_value"):
|
|
192
|
+
return "0.2e"
|
|
193
|
+
elif any(x in col_lower for x in ("r2", "r²", "ld")):
|
|
194
|
+
return "0.3f"
|
|
195
|
+
elif "pos" in col_lower:
|
|
196
|
+
return "0,0"
|
|
197
|
+
|
|
198
|
+
return None
|