pylocuszoom 1.0.0__py3-none-any.whl → 1.1.1__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 +9 -1
- pylocuszoom/_plotter_utils.py +66 -0
- pylocuszoom/backends/base.py +56 -0
- pylocuszoom/backends/bokeh_backend.py +141 -29
- pylocuszoom/backends/matplotlib_backend.py +60 -0
- pylocuszoom/backends/plotly_backend.py +297 -88
- pylocuszoom/ensembl.py +6 -11
- pylocuszoom/gene_track.py +2 -24
- pylocuszoom/labels.py +6 -2
- pylocuszoom/manhattan.py +246 -0
- pylocuszoom/manhattan_plotter.py +760 -0
- pylocuszoom/plotter.py +236 -270
- pylocuszoom/qq.py +123 -0
- pylocuszoom/recombination.py +7 -7
- pylocuszoom/stats_plotter.py +319 -0
- {pylocuszoom-1.0.0.dist-info → pylocuszoom-1.1.1.dist-info}/METADATA +130 -20
- pylocuszoom-1.1.1.dist-info/RECORD +36 -0
- pylocuszoom-1.0.0.dist-info/RECORD +0 -31
- {pylocuszoom-1.0.0.dist-info → pylocuszoom-1.1.1.dist-info}/WHEEL +0 -0
- {pylocuszoom-1.0.0.dist-info → pylocuszoom-1.1.1.dist-info}/licenses/LICENSE.md +0 -0
pylocuszoom/__init__.py
CHANGED
|
@@ -34,7 +34,7 @@ Species Support:
|
|
|
34
34
|
- Custom: User provides all reference data
|
|
35
35
|
"""
|
|
36
36
|
|
|
37
|
-
__version__ = "
|
|
37
|
+
__version__ = "1.1.1"
|
|
38
38
|
|
|
39
39
|
# Main plotter class
|
|
40
40
|
# Backend types
|
|
@@ -133,6 +133,9 @@ from .loaders import (
|
|
|
133
133
|
# Logging configuration
|
|
134
134
|
from .logging import disable_logging, enable_logging
|
|
135
135
|
|
|
136
|
+
# Manhattan and QQ plotting
|
|
137
|
+
from .manhattan_plotter import ManhattanPlotter
|
|
138
|
+
|
|
136
139
|
# PheWAS support
|
|
137
140
|
from .phewas import validate_phewas_df
|
|
138
141
|
from .plotter import LocusZoomPlotter
|
|
@@ -145,6 +148,9 @@ from .recombination import (
|
|
|
145
148
|
load_recombination_map,
|
|
146
149
|
)
|
|
147
150
|
|
|
151
|
+
# Statistical visualizations (PheWAS, forest plots)
|
|
152
|
+
from .stats_plotter import StatsPlotter
|
|
153
|
+
|
|
148
154
|
# Validation utilities
|
|
149
155
|
from .utils import to_pandas
|
|
150
156
|
|
|
@@ -152,6 +158,8 @@ __all__ = [
|
|
|
152
158
|
# Core
|
|
153
159
|
"__version__",
|
|
154
160
|
"LocusZoomPlotter",
|
|
161
|
+
"ManhattanPlotter",
|
|
162
|
+
"StatsPlotter",
|
|
155
163
|
# Backends
|
|
156
164
|
"BackendType",
|
|
157
165
|
"get_backend",
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""Shared utilities for plotter classes.
|
|
2
|
+
|
|
3
|
+
Internal module - not part of public API.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from typing import Any, Optional
|
|
7
|
+
|
|
8
|
+
import numpy as np
|
|
9
|
+
import pandas as pd
|
|
10
|
+
|
|
11
|
+
# Significance thresholds
|
|
12
|
+
DEFAULT_GENOMEWIDE_THRESHOLD = 5e-8
|
|
13
|
+
|
|
14
|
+
# Manhattan/QQ plot styling constants
|
|
15
|
+
MANHATTAN_POINT_SIZE = 10
|
|
16
|
+
MANHATTAN_CATEGORICAL_POINT_SIZE = 30
|
|
17
|
+
QQ_POINT_SIZE = 10
|
|
18
|
+
POINT_EDGE_COLOR = "black"
|
|
19
|
+
MANHATTAN_EDGE_WIDTH = 0.1
|
|
20
|
+
QQ_EDGE_WIDTH = 0.02
|
|
21
|
+
QQ_POINT_COLOR = "#1f77b4"
|
|
22
|
+
QQ_CI_COLOR = "#CCCCCC"
|
|
23
|
+
QQ_CI_ALPHA = 0.5
|
|
24
|
+
SIGNIFICANCE_LINE_COLOR = "red"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def transform_pvalues(df: pd.DataFrame, p_col: str) -> pd.DataFrame:
|
|
28
|
+
"""Add neglog10p column with -log10 transformed p-values.
|
|
29
|
+
|
|
30
|
+
Clips extremely small p-values to 1e-300 to avoid -inf.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
df: DataFrame with p-value column.
|
|
34
|
+
p_col: Name of p-value column.
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
DataFrame with neglog10p column added.
|
|
38
|
+
"""
|
|
39
|
+
df = df.copy()
|
|
40
|
+
df["neglog10p"] = -np.log10(df[p_col].clip(lower=1e-300))
|
|
41
|
+
return df
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def add_significance_line(
|
|
45
|
+
backend: Any,
|
|
46
|
+
ax: Any,
|
|
47
|
+
threshold: Optional[float],
|
|
48
|
+
) -> None:
|
|
49
|
+
"""Add genome-wide significance threshold line.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
backend: Plot backend instance.
|
|
53
|
+
ax: Axes object from backend.
|
|
54
|
+
threshold: P-value threshold (e.g., 5e-8). None to skip.
|
|
55
|
+
"""
|
|
56
|
+
if threshold is None:
|
|
57
|
+
return
|
|
58
|
+
threshold_line = -np.log10(threshold)
|
|
59
|
+
backend.axhline(
|
|
60
|
+
ax,
|
|
61
|
+
y=threshold_line,
|
|
62
|
+
color=SIGNIFICANCE_LINE_COLOR,
|
|
63
|
+
linestyle="--",
|
|
64
|
+
linewidth=1,
|
|
65
|
+
zorder=1,
|
|
66
|
+
)
|
pylocuszoom/backends/base.py
CHANGED
|
@@ -96,6 +96,31 @@ class PlotBackend(Protocol):
|
|
|
96
96
|
"""
|
|
97
97
|
...
|
|
98
98
|
|
|
99
|
+
def create_figure_grid(
|
|
100
|
+
self,
|
|
101
|
+
n_rows: int,
|
|
102
|
+
n_cols: int,
|
|
103
|
+
width_ratios: Optional[List[float]] = None,
|
|
104
|
+
height_ratios: Optional[List[float]] = None,
|
|
105
|
+
figsize: Tuple[float, float] = (12.0, 8.0),
|
|
106
|
+
) -> Tuple[Any, List[Any]]:
|
|
107
|
+
"""Create a figure with a grid of subplots.
|
|
108
|
+
|
|
109
|
+
Unlike create_figure which creates vertically stacked panels,
|
|
110
|
+
this creates a 2D grid of subplots.
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
n_rows: Number of rows.
|
|
114
|
+
n_cols: Number of columns.
|
|
115
|
+
width_ratios: Relative widths for columns.
|
|
116
|
+
height_ratios: Relative heights for rows.
|
|
117
|
+
figsize: Figure size as (width, height).
|
|
118
|
+
|
|
119
|
+
Returns:
|
|
120
|
+
Tuple of (figure, flattened list of axes).
|
|
121
|
+
"""
|
|
122
|
+
...
|
|
123
|
+
|
|
99
124
|
# =========================================================================
|
|
100
125
|
# Basic Plotting
|
|
101
126
|
# =========================================================================
|
|
@@ -442,6 +467,27 @@ class PlotBackend(Protocol):
|
|
|
442
467
|
"""
|
|
443
468
|
...
|
|
444
469
|
|
|
470
|
+
def set_xticks(
|
|
471
|
+
self,
|
|
472
|
+
ax: Any,
|
|
473
|
+
positions: List[float],
|
|
474
|
+
labels: List[str],
|
|
475
|
+
fontsize: int = 10,
|
|
476
|
+
rotation: int = 0,
|
|
477
|
+
ha: str = "center",
|
|
478
|
+
) -> None:
|
|
479
|
+
"""Set x-axis tick positions and labels.
|
|
480
|
+
|
|
481
|
+
Args:
|
|
482
|
+
ax: Axes or panel.
|
|
483
|
+
positions: Tick positions.
|
|
484
|
+
labels: Tick labels.
|
|
485
|
+
fontsize: Font size.
|
|
486
|
+
rotation: Label rotation in degrees.
|
|
487
|
+
ha: Horizontal alignment for rotated labels.
|
|
488
|
+
"""
|
|
489
|
+
...
|
|
490
|
+
|
|
445
491
|
def set_title(self, ax: Any, title: str, fontsize: int = 14) -> None:
|
|
446
492
|
"""Set panel title.
|
|
447
493
|
|
|
@@ -452,6 +498,16 @@ class PlotBackend(Protocol):
|
|
|
452
498
|
"""
|
|
453
499
|
...
|
|
454
500
|
|
|
501
|
+
def set_suptitle(self, fig: Any, title: str, fontsize: int = 14) -> None:
|
|
502
|
+
"""Set overall figure title (super title).
|
|
503
|
+
|
|
504
|
+
Args:
|
|
505
|
+
fig: Figure object.
|
|
506
|
+
title: Title text.
|
|
507
|
+
fontsize: Font size.
|
|
508
|
+
"""
|
|
509
|
+
...
|
|
510
|
+
|
|
455
511
|
def hide_spines(self, ax: Any, spines: List[str]) -> None:
|
|
456
512
|
"""Hide specified axis spines.
|
|
457
513
|
|
|
@@ -6,13 +6,27 @@ Interactive backend with hover tooltips, well-suited for dashboards.
|
|
|
6
6
|
from typing import Any, List, Optional, Tuple, Union
|
|
7
7
|
|
|
8
8
|
import pandas as pd
|
|
9
|
-
from bokeh.
|
|
10
|
-
from bokeh.layouts import column
|
|
9
|
+
from bokeh.layouts import column, row
|
|
11
10
|
from bokeh.models import ColumnDataSource, DataRange1d, HoverTool, Span
|
|
12
11
|
from bokeh.plotting import figure
|
|
13
12
|
|
|
14
13
|
from . import convert_latex_to_unicode, register_backend
|
|
15
14
|
|
|
15
|
+
# Style mappings (matplotlib -> Bokeh)
|
|
16
|
+
_MARKER_MAP = {
|
|
17
|
+
"o": "circle",
|
|
18
|
+
"D": "diamond",
|
|
19
|
+
"s": "square",
|
|
20
|
+
"^": "triangle",
|
|
21
|
+
"v": "inverted_triangle",
|
|
22
|
+
}
|
|
23
|
+
_DASH_MAP = {
|
|
24
|
+
"-": "solid",
|
|
25
|
+
"--": "dashed",
|
|
26
|
+
":": "dotted",
|
|
27
|
+
"-.": "dashdot",
|
|
28
|
+
}
|
|
29
|
+
|
|
16
30
|
|
|
17
31
|
@register_backend("bokeh")
|
|
18
32
|
class BokehBackend:
|
|
@@ -22,21 +36,6 @@ class BokehBackend:
|
|
|
22
36
|
applications and dashboards.
|
|
23
37
|
"""
|
|
24
38
|
|
|
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
39
|
@property
|
|
41
40
|
def supports_snp_labels(self) -> bool:
|
|
42
41
|
"""Bokeh uses hover tooltips instead of labels."""
|
|
@@ -105,6 +104,73 @@ class BokehBackend:
|
|
|
105
104
|
|
|
106
105
|
return layout, figures
|
|
107
106
|
|
|
107
|
+
def create_figure_grid(
|
|
108
|
+
self,
|
|
109
|
+
n_rows: int,
|
|
110
|
+
n_cols: int,
|
|
111
|
+
width_ratios: Optional[List[float]] = None,
|
|
112
|
+
height_ratios: Optional[List[float]] = None,
|
|
113
|
+
figsize: Tuple[float, float] = (12.0, 8.0),
|
|
114
|
+
) -> Tuple[Any, List[figure]]:
|
|
115
|
+
"""Create a layout with a grid of subplots.
|
|
116
|
+
|
|
117
|
+
Args:
|
|
118
|
+
n_rows: Number of rows.
|
|
119
|
+
n_cols: Number of columns.
|
|
120
|
+
width_ratios: Relative widths for columns.
|
|
121
|
+
height_ratios: Relative heights for rows.
|
|
122
|
+
figsize: Figure size as (width, height).
|
|
123
|
+
|
|
124
|
+
Returns:
|
|
125
|
+
Tuple of (layout, flattened list of figure objects).
|
|
126
|
+
"""
|
|
127
|
+
width_px = int(figsize[0] * 100)
|
|
128
|
+
height_px = int(figsize[1] * 100)
|
|
129
|
+
|
|
130
|
+
# Calculate widths
|
|
131
|
+
if width_ratios is not None:
|
|
132
|
+
total_w = sum(width_ratios)
|
|
133
|
+
widths = [int(width_px * w / total_w) for w in width_ratios]
|
|
134
|
+
else:
|
|
135
|
+
widths = [width_px // n_cols] * n_cols
|
|
136
|
+
|
|
137
|
+
# Calculate heights
|
|
138
|
+
if height_ratios is not None:
|
|
139
|
+
total_h = sum(height_ratios)
|
|
140
|
+
heights = [int(height_px * h / total_h) for h in height_ratios]
|
|
141
|
+
else:
|
|
142
|
+
heights = [height_px // n_rows] * n_rows
|
|
143
|
+
|
|
144
|
+
figures = []
|
|
145
|
+
rows = []
|
|
146
|
+
|
|
147
|
+
for i in range(n_rows):
|
|
148
|
+
row_figures = []
|
|
149
|
+
for j in range(n_cols):
|
|
150
|
+
p = figure(
|
|
151
|
+
width=widths[j],
|
|
152
|
+
height=heights[i],
|
|
153
|
+
tools="pan,wheel_zoom,box_zoom,reset,save",
|
|
154
|
+
toolbar_location="above" if i == 0 and j == 0 else None,
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
# Style
|
|
158
|
+
p.grid.visible = False
|
|
159
|
+
p.outline_line_color = None
|
|
160
|
+
p.xaxis.axis_line_color = "black"
|
|
161
|
+
p.yaxis.axis_line_color = "black"
|
|
162
|
+
p.xaxis.minor_tick_line_color = None
|
|
163
|
+
p.yaxis.minor_tick_line_color = None
|
|
164
|
+
|
|
165
|
+
row_figures.append(p)
|
|
166
|
+
figures.append(p)
|
|
167
|
+
|
|
168
|
+
rows.append(row(*row_figures))
|
|
169
|
+
|
|
170
|
+
layout = column(*rows)
|
|
171
|
+
|
|
172
|
+
return layout, figures
|
|
173
|
+
|
|
108
174
|
def scatter(
|
|
109
175
|
self,
|
|
110
176
|
ax: figure,
|
|
@@ -153,8 +219,7 @@ class BokehBackend:
|
|
|
153
219
|
|
|
154
220
|
source = ColumnDataSource(data)
|
|
155
221
|
|
|
156
|
-
|
|
157
|
-
marker_type = self._MARKER_MAP.get(marker, "circle")
|
|
222
|
+
marker_type = _MARKER_MAP.get(marker, "circle")
|
|
158
223
|
|
|
159
224
|
# Create scatter using scatter() method (Bokeh 3.4+ preferred API)
|
|
160
225
|
scatter_kwargs = {
|
|
@@ -194,7 +259,7 @@ class BokehBackend:
|
|
|
194
259
|
label: Optional[str] = None,
|
|
195
260
|
) -> Any:
|
|
196
261
|
"""Create a line plot on the given figure."""
|
|
197
|
-
line_dash =
|
|
262
|
+
line_dash = _DASH_MAP.get(linestyle, "solid")
|
|
198
263
|
|
|
199
264
|
line_kwargs = {
|
|
200
265
|
"line_color": color,
|
|
@@ -249,7 +314,7 @@ class BokehBackend:
|
|
|
249
314
|
zorder: int = 1,
|
|
250
315
|
) -> Any:
|
|
251
316
|
"""Add a horizontal line across the figure."""
|
|
252
|
-
line_dash =
|
|
317
|
+
line_dash = _DASH_MAP.get(linestyle, "dashed")
|
|
253
318
|
|
|
254
319
|
span = Span(
|
|
255
320
|
location=y,
|
|
@@ -379,9 +444,31 @@ class BokehBackend:
|
|
|
379
444
|
) -> None:
|
|
380
445
|
"""Set y-axis tick positions and labels."""
|
|
381
446
|
ax.yaxis.ticker = positions
|
|
382
|
-
ax.yaxis.major_label_overrides =
|
|
447
|
+
ax.yaxis.major_label_overrides = {
|
|
448
|
+
pos: label for pos, label in zip(positions, labels)
|
|
449
|
+
}
|
|
383
450
|
ax.yaxis.major_label_text_font_size = f"{fontsize}pt"
|
|
384
451
|
|
|
452
|
+
def set_xticks(
|
|
453
|
+
self,
|
|
454
|
+
ax: figure,
|
|
455
|
+
positions: List[float],
|
|
456
|
+
labels: List[str],
|
|
457
|
+
fontsize: int = 10,
|
|
458
|
+
rotation: int = 0,
|
|
459
|
+
ha: str = "center",
|
|
460
|
+
) -> None:
|
|
461
|
+
"""Set x-axis tick positions and labels."""
|
|
462
|
+
ax.xaxis.ticker = positions
|
|
463
|
+
ax.xaxis.major_label_overrides = {
|
|
464
|
+
pos: label for pos, label in zip(positions, labels)
|
|
465
|
+
}
|
|
466
|
+
ax.xaxis.major_label_text_font_size = f"{fontsize}pt"
|
|
467
|
+
if rotation:
|
|
468
|
+
ax.xaxis.major_label_orientation = (
|
|
469
|
+
rotation * 3.14159 / 180
|
|
470
|
+
) # Convert to radians
|
|
471
|
+
|
|
385
472
|
def _get_legend_location(self, loc: str, default: str = "top_left") -> str:
|
|
386
473
|
"""Map matplotlib-style legend location to Bokeh location."""
|
|
387
474
|
loc_map = {
|
|
@@ -401,6 +488,22 @@ class BokehBackend:
|
|
|
401
488
|
ax.title.text = title
|
|
402
489
|
ax.title.text_font_size = f"{fontsize}pt"
|
|
403
490
|
|
|
491
|
+
def set_suptitle(self, fig: Any, title: str, fontsize: int = 14) -> None:
|
|
492
|
+
"""Set overall figure title.
|
|
493
|
+
|
|
494
|
+
For Bokeh layouts, add title to the first figure in the layout.
|
|
495
|
+
"""
|
|
496
|
+
from bokeh.models.layouts import Column
|
|
497
|
+
|
|
498
|
+
if isinstance(fig, Column) and len(fig.children) > 0:
|
|
499
|
+
first_child = fig.children[0]
|
|
500
|
+
if hasattr(first_child, "title"):
|
|
501
|
+
first_child.title.text = title
|
|
502
|
+
first_child.title.text_font_size = f"{fontsize}pt"
|
|
503
|
+
elif hasattr(fig, "title"):
|
|
504
|
+
fig.title.text = title
|
|
505
|
+
fig.title.text_font_size = f"{fontsize}pt"
|
|
506
|
+
|
|
404
507
|
def create_twin_axis(self, ax: figure) -> Any:
|
|
405
508
|
"""Create a secondary y-axis.
|
|
406
509
|
|
|
@@ -408,9 +511,15 @@ class BokehBackend:
|
|
|
408
511
|
"""
|
|
409
512
|
from bokeh.models import LinearAxis, Range1d
|
|
410
513
|
|
|
411
|
-
# Add a second y-axis
|
|
514
|
+
# Add a second y-axis without tick marks (cleaner look)
|
|
412
515
|
ax.extra_y_ranges = {"secondary": Range1d(start=0, end=100)}
|
|
413
|
-
|
|
516
|
+
secondary_axis = LinearAxis(
|
|
517
|
+
y_range_name="secondary",
|
|
518
|
+
major_tick_line_color=None, # Hide major ticks
|
|
519
|
+
minor_tick_line_color=None, # Hide minor ticks
|
|
520
|
+
major_label_text_font_size="0pt", # Hide tick labels
|
|
521
|
+
)
|
|
522
|
+
ax.add_layout(secondary_axis, "right")
|
|
414
523
|
|
|
415
524
|
return "secondary"
|
|
416
525
|
|
|
@@ -427,7 +536,7 @@ class BokehBackend:
|
|
|
427
536
|
yaxis_name: str = "secondary",
|
|
428
537
|
) -> Any:
|
|
429
538
|
"""Create a line plot on secondary y-axis."""
|
|
430
|
-
line_dash =
|
|
539
|
+
line_dash = _DASH_MAP.get(linestyle, "solid")
|
|
431
540
|
|
|
432
541
|
return ax.line(
|
|
433
542
|
x.values,
|
|
@@ -570,7 +679,7 @@ class BokehBackend:
|
|
|
570
679
|
label: str,
|
|
571
680
|
color: str,
|
|
572
681
|
marker: str,
|
|
573
|
-
size: int =
|
|
682
|
+
size: int = 14,
|
|
574
683
|
) -> Any:
|
|
575
684
|
"""Create an invisible scatter renderer for a legend entry."""
|
|
576
685
|
from bokeh.models import LegendItem
|
|
@@ -619,7 +728,7 @@ class BokehBackend:
|
|
|
619
728
|
"""
|
|
620
729
|
source = self._ensure_legend_range(ax)
|
|
621
730
|
items = [
|
|
622
|
-
self._add_legend_item(ax, source, "Lead SNP", lead_snp_color, "diamond",
|
|
731
|
+
self._add_legend_item(ax, source, "Lead SNP", lead_snp_color, "diamond", 16)
|
|
623
732
|
]
|
|
624
733
|
for _, label, color in ld_bins:
|
|
625
734
|
items.append(self._add_legend_item(ax, source, label, color, "square"))
|
|
@@ -674,6 +783,8 @@ class BokehBackend:
|
|
|
674
783
|
|
|
675
784
|
Supports .html for interactive and .png for static.
|
|
676
785
|
"""
|
|
786
|
+
from bokeh.io import export_png, export_svgs, output_file, save
|
|
787
|
+
|
|
677
788
|
if path.endswith(".html"):
|
|
678
789
|
output_file(path)
|
|
679
790
|
save(fig)
|
|
@@ -682,12 +793,13 @@ class BokehBackend:
|
|
|
682
793
|
elif path.endswith(".svg"):
|
|
683
794
|
export_svgs(fig, filename=path)
|
|
684
795
|
else:
|
|
685
|
-
# Default to HTML
|
|
686
796
|
output_file(path)
|
|
687
797
|
save(fig)
|
|
688
798
|
|
|
689
799
|
def show(self, fig: Any) -> None:
|
|
690
800
|
"""Display the figure."""
|
|
801
|
+
from bokeh.io import show
|
|
802
|
+
|
|
691
803
|
show(fig)
|
|
692
804
|
|
|
693
805
|
def close(self, fig: Any) -> None:
|
|
@@ -756,7 +868,7 @@ class BokehBackend:
|
|
|
756
868
|
zorder: int = 1,
|
|
757
869
|
) -> Any:
|
|
758
870
|
"""Add a vertical line across the figure."""
|
|
759
|
-
line_dash =
|
|
871
|
+
line_dash = _DASH_MAP.get(linestyle, "dashed")
|
|
760
872
|
|
|
761
873
|
span = Span(
|
|
762
874
|
location=x,
|
|
@@ -82,6 +82,48 @@ class MatplotlibBackend:
|
|
|
82
82
|
|
|
83
83
|
return fig, list(axes)
|
|
84
84
|
|
|
85
|
+
def create_figure_grid(
|
|
86
|
+
self,
|
|
87
|
+
n_rows: int,
|
|
88
|
+
n_cols: int,
|
|
89
|
+
width_ratios: Optional[List[float]] = None,
|
|
90
|
+
height_ratios: Optional[List[float]] = None,
|
|
91
|
+
figsize: Tuple[float, float] = (12.0, 8.0),
|
|
92
|
+
) -> Tuple[Figure, List[Axes]]:
|
|
93
|
+
"""Create a figure with a grid of subplots.
|
|
94
|
+
|
|
95
|
+
Args:
|
|
96
|
+
n_rows: Number of rows.
|
|
97
|
+
n_cols: Number of columns.
|
|
98
|
+
width_ratios: Relative widths for columns.
|
|
99
|
+
height_ratios: Relative heights for rows.
|
|
100
|
+
figsize: Figure size as (width, height).
|
|
101
|
+
|
|
102
|
+
Returns:
|
|
103
|
+
Tuple of (figure, flattened list of axes).
|
|
104
|
+
"""
|
|
105
|
+
plt.ioff()
|
|
106
|
+
|
|
107
|
+
gridspec_kw = {}
|
|
108
|
+
if width_ratios is not None:
|
|
109
|
+
gridspec_kw["width_ratios"] = width_ratios
|
|
110
|
+
if height_ratios is not None:
|
|
111
|
+
gridspec_kw["height_ratios"] = height_ratios
|
|
112
|
+
|
|
113
|
+
fig, axes = plt.subplots(
|
|
114
|
+
n_rows,
|
|
115
|
+
n_cols,
|
|
116
|
+
figsize=figsize,
|
|
117
|
+
gridspec_kw=gridspec_kw if gridspec_kw else None,
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
# Flatten axes to list
|
|
121
|
+
import numpy as np
|
|
122
|
+
|
|
123
|
+
if isinstance(axes, np.ndarray):
|
|
124
|
+
return fig, list(axes.flatten())
|
|
125
|
+
return fig, [axes]
|
|
126
|
+
|
|
85
127
|
def scatter(
|
|
86
128
|
self,
|
|
87
129
|
ax: Axes,
|
|
@@ -320,6 +362,19 @@ class MatplotlibBackend:
|
|
|
320
362
|
ax.set_yticks(positions)
|
|
321
363
|
ax.set_yticklabels(labels, fontsize=fontsize)
|
|
322
364
|
|
|
365
|
+
def set_xticks(
|
|
366
|
+
self,
|
|
367
|
+
ax: Axes,
|
|
368
|
+
positions: List[float],
|
|
369
|
+
labels: List[str],
|
|
370
|
+
fontsize: int = 10,
|
|
371
|
+
rotation: int = 0,
|
|
372
|
+
ha: str = "center",
|
|
373
|
+
) -> None:
|
|
374
|
+
"""Set x-axis tick positions and labels."""
|
|
375
|
+
ax.set_xticks(positions)
|
|
376
|
+
ax.set_xticklabels(labels, fontsize=fontsize, rotation=rotation, ha=ha)
|
|
377
|
+
|
|
323
378
|
def set_title(self, ax: Axes, title: str, fontsize: int = 14) -> None:
|
|
324
379
|
"""Set panel title."""
|
|
325
380
|
ax.set_title(
|
|
@@ -329,6 +384,10 @@ class MatplotlibBackend:
|
|
|
329
384
|
fontfamily="sans-serif",
|
|
330
385
|
)
|
|
331
386
|
|
|
387
|
+
def set_suptitle(self, fig: Figure, title: str, fontsize: int = 14) -> None:
|
|
388
|
+
"""Set overall figure title (super title)."""
|
|
389
|
+
fig.suptitle(title, fontsize=fontsize, fontweight="bold")
|
|
390
|
+
|
|
332
391
|
def create_twin_axis(self, ax: Axes) -> Axes:
|
|
333
392
|
"""Create a secondary y-axis sharing the same x-axis."""
|
|
334
393
|
return ax.twinx()
|
|
@@ -445,6 +504,7 @@ class MatplotlibBackend:
|
|
|
445
504
|
yaxis_name: Ignored for matplotlib.
|
|
446
505
|
"""
|
|
447
506
|
ax.set_ylabel(label, fontsize=fontsize, color=color)
|
|
507
|
+
ax.tick_params(axis="y", labelcolor=color, labelsize=fontsize - 1)
|
|
448
508
|
|
|
449
509
|
def add_legend(
|
|
450
510
|
self,
|