Skip to content

Commit bf74abb

Browse files
authored
Merge pull request #68 from moshi4/develop
Bump to v1.5.0
2 parents 45ad937 + a2138d2 commit bf74abb

12 files changed

+835
-711
lines changed

.pre-commit-config.yaml

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
# See https://pre-commit.com/hooks.html for more hooks
33
repos:
44
- repo: https://github.com/astral-sh/ruff-pre-commit
5-
rev: v0.3.1
5+
rev: v0.4.4
66
hooks:
77
- id: ruff
88
name: ruff lint check

docs/radar_chart.ipynb

+77
Large diffs are not rendered by default.

poetry.lock

+623-639
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = "pyCirclize"
3-
version = "1.4.0"
3+
version = "1.5.0"
44
description = "Circular visualization in Python"
55
authors = ["moshi4"]
66
license = "MIT"

src/pycirclize/__init__.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from pycirclize.circos import Circos
22

3-
__version__ = "1.4.0"
3+
__version__ = "1.5.0"
44

55
__all__ = [
66
"Circos",

src/pycirclize/circos.py

+44-22
Original file line numberDiff line numberDiff line change
@@ -40,21 +40,20 @@ class Circos:
4040

4141
def __init__(
4242
self,
43-
sectors: Mapping[str, int | float],
43+
sectors: Mapping[str, int | float | tuple[float, float]],
4444
start: float = 0,
4545
end: float = 360,
4646
*,
4747
space: float | list[float] = 0,
4848
endspace: bool = True,
49-
sector2start_pos: Mapping[str, int | float] | None = None,
5049
sector2clockwise: dict[str, bool] | None = None,
5150
show_axis_for_debug: bool = False,
5251
):
5352
"""
5453
Parameters
5554
----------
56-
sectors : Mapping[str, int | float]
57-
Sector name & size dict
55+
sectors : Mapping[str, int | float | tuple[float, float]]
56+
Sector name & size (or range) dict
5857
start : float, optional
5958
Plot start degree (`-360 <= start < end <= 360`)
6059
end : float, optional
@@ -63,14 +62,11 @@ def __init__(
6362
Space degree(s) between sector
6463
endspace : bool, optional
6564
If True, insert space after the end sector
66-
sector2start_pos : Mapping[str, int | float] | None, optional
67-
Sector name & start position dict. By default, `start_pos=0`.
6865
sector2clockwise : dict[str, bool] | None, optional
6966
Sector name & clockwise bool dict. By default, `clockwise=True`.
7067
show_axis_for_debug : bool, optional
7168
Show axis for position check debugging (Developer option)
7269
"""
73-
sector2start_pos = {} if sector2start_pos is None else sector2start_pos
7470
sector2clockwise = {} if sector2clockwise is None else sector2clockwise
7571

7672
# Check start-end degree range
@@ -100,19 +96,21 @@ def __init__(
10096
"""
10197
)[1:-1]
10298
raise ValueError(err_msg)
103-
sector_total_size = sum(sectors.values())
99+
100+
sector2range = self._to_sector2range(sectors)
101+
sector_total_size = sum([max(r) - min(r) for r in sector2range.values()])
104102

105103
rad_pos = math.radians(start)
106104
self._sectors: list[Sector] = []
107-
for idx, (sector_name, sector_size) in enumerate(sectors.items()):
105+
for idx, (sector_name, sector_range) in enumerate(sector2range.items()):
106+
sector_size = max(sector_range) - min(sector_range)
108107
sector_size_ratio = sector_size / sector_total_size
109108
deg_size = whole_deg_size_without_space * sector_size_ratio
110109
rad_size = math.radians(deg_size)
111110
rad_lim = (rad_pos, rad_pos + rad_size)
112111
rad_pos += rad_size + math.radians(space_list[idx])
113-
start_pos = sector2start_pos.get(sector_name, 0)
114112
clockwise = sector2clockwise.get(sector_name, True)
115-
sector = Sector(sector_name, sector_size, rad_lim, start_pos, clockwise)
113+
sector = Sector(sector_name, sector_range, rad_lim, clockwise)
116114
self._sectors.append(sector)
117115

118116
self._deg_lim = (start, end)
@@ -180,6 +178,7 @@ def radar_chart(
180178
table: str | Path | pd.DataFrame | RadarTable,
181179
*,
182180
r_lim: tuple[float, float] = (0, 100),
181+
vmin: float = 0,
183182
vmax: float = 100,
184183
fill: bool = True,
185184
marker_size: int = 0,
@@ -203,6 +202,8 @@ def radar_chart(
203202
Table file or Table dataframe or RadarTable instance
204203
r_lim : tuple[float, float], optional
205204
Radar chart radius limit region (0 - 100)
205+
vmin : float, optional
206+
Min value
206207
vmax : float, optional
207208
Max value
208209
fill : bool, optional
@@ -244,6 +245,10 @@ def radar_chart(
244245
circos : Circos
245246
Circos instance initialized for radar chart
246247
"""
248+
if not vmin < vmax:
249+
raise ValueError(f"vmax must be larger than vmin ({vmin=}, {vmax=})")
250+
size = vmax - vmin
251+
247252
# Setup default properties
248253
grid_line_kws = {} if grid_line_kws is None else deepcopy(grid_line_kws)
249254
for k, v in dict(color="grey", ls="dashed", lw=0.5).items():
@@ -269,11 +274,12 @@ def radar_chart(
269274
if not 0 < grid_interval_ratio <= 1.0:
270275
raise ValueError(f"{grid_interval_ratio=} is invalid.")
271276
# Plot horizontal grid line & label
272-
stop, step = vmax + (vmax / 1000), vmax * grid_interval_ratio
273-
for v in np.arange(0, stop, step):
274-
track.line(x, [v] * len(x), vmax=vmax, arc=circular, **grid_line_kws)
277+
stop, step = vmax + (size / 1000), size * grid_interval_ratio
278+
for v in np.arange(vmin, stop, step):
279+
y = [v] * len(x)
280+
track.line(x, y, vmin=vmin, vmax=vmax, arc=circular, **grid_line_kws)
275281
if show_grid_label:
276-
r = track._y_to_r(v, 0, vmax)
282+
r = track._y_to_r(v, vmin, vmax)
277283
# Format grid label
278284
if grid_label_formatter:
279285
text = grid_label_formatter(v)
@@ -283,7 +289,7 @@ def radar_chart(
283289
track.text(text, 0, r, **grid_label_kws)
284290
# Plot vertical grid line
285291
for p in x[:-1]:
286-
track.line([p, p], [0, vmax], vmax=vmax, **grid_line_kws)
292+
track.line([p, p], [vmin, vmax], vmin=vmin, vmax=vmax, **grid_line_kws)
287293

288294
# Plot radar charts
289295
if isinstance(cmap, str):
@@ -296,15 +302,16 @@ def radar_chart(
296302
line_kws = line_kws_handler(row_name) if line_kws_handler else {}
297303
line_kws.setdefault("lw", 1.0)
298304
line_kws.setdefault("label", row_name)
299-
track.line(x, y, vmax=vmax, arc=False, color=color, **line_kws)
305+
track.line(x, y, vmin=vmin, vmax=vmax, arc=False, color=color, **line_kws)
300306
if marker_size > 0:
301307
marker_kws = marker_kws_handler(row_name) if marker_kws_handler else {}
302308
marker_kws.setdefault("marker", "o")
303309
marker_kws.setdefault("zorder", 2)
304310
marker_kws.update(s=marker_size**2)
305-
track.scatter(x, y, vmax=vmax, color=color, **marker_kws)
311+
track.scatter(x, y, vmin=vmin, vmax=vmax, color=color, **marker_kws)
306312
if fill:
307-
track.fill_between(x, y, vmax=vmax, arc=False, color=color, alpha=0.5)
313+
fill_kws = dict(arc=False, color=color, alpha=0.5)
314+
track.fill_between(x, y, y2=vmin, vmin=vmin, vmax=vmax, **fill_kws) # type:ignore
308315

309316
# Plot column names
310317
for idx, col_name in enumerate(radar_table.col_names):
@@ -577,15 +584,13 @@ def initialize_from_bed(
577584
Circos instance initialized from BED file
578585
"""
579586
records = Bed(bed_file).records
580-
sectors = {rec.chr: rec.size for rec in records}
581-
sector2start_pos = {rec.chr: rec.start for rec in records}
587+
sectors = {rec.chr: (rec.start, rec.end) for rec in records}
582588
return Circos(
583589
sectors,
584590
start,
585591
end,
586592
space=space,
587593
endspace=endspace,
588-
sector2start_pos=sector2start_pos,
589594
sector2clockwise=sector2clockwise,
590595
)
591596

@@ -1098,6 +1103,23 @@ def _check_degree_range(self, start: float, end: float) -> None:
10981103
err_msg = f"'end - start' must be less than {max_deg} ({start=}, {end=})"
10991104
raise ValueError(err_msg)
11001105

1106+
def _to_sector2range(
1107+
self,
1108+
sectors: Mapping[str, int | float | tuple[float, float]],
1109+
) -> dict[str, tuple[float, float]]:
1110+
"""Convert sectors to sector2range"""
1111+
sector2range: dict[str, tuple[float, float]] = {}
1112+
for name, value in sectors.items():
1113+
if isinstance(value, (tuple, list)):
1114+
sector_start, sector_end = value
1115+
if not sector_start < sector_end:
1116+
err_msg = f"{sector_end=} must be larger than {sector_start=}."
1117+
raise ValueError(err_msg)
1118+
sector2range[name] = (sector_start, sector_end)
1119+
else:
1120+
sector2range[name] = (0, value)
1121+
return sector2range
1122+
11011123
def _initialize_figure(
11021124
self,
11031125
figsize: tuple[float, float] = (8, 8),

src/pycirclize/parser/genbank.py

+29-12
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,16 @@
77
from collections import defaultdict
88
from io import StringIO, TextIOWrapper
99
from pathlib import Path
10+
from typing import TYPE_CHECKING
1011

1112
import numpy as np
1213
from Bio import SeqIO, SeqUtils
1314
from Bio.SeqFeature import Seq, SeqFeature, SimpleLocation
1415
from Bio.SeqRecord import SeqRecord
1516

17+
if TYPE_CHECKING:
18+
from numpy.typing import NDArray
19+
1620

1721
class Genbank:
1822
"""Genbank Parser Class"""
@@ -56,11 +60,14 @@ def __init__(
5660
elif isinstance(self._gbk_source, (StringIO, TextIOWrapper)):
5761
self._name = self._records[0].name
5862
else:
59-
raise NotImplementedError("Failed to get name.")
63+
raise ValueError("Failed to get genbank name.")
6064

6165
if min_range or max_range:
6266
warnings.warn("min_range & max_range is no longer used in Genbank parser.")
6367

68+
if len(self.records) == 0:
69+
raise ValueError(f"Failed to parse {gbk_source} as Genbank file.")
70+
6471
############################################################
6572
# Property
6673
############################################################
@@ -127,7 +134,7 @@ def calc_gc_skew(
127134
step_size: int | None = None,
128135
*,
129136
seq: str | None = None,
130-
) -> tuple[np.ndarray, np.ndarray]:
137+
) -> tuple[NDArray[np.int64], NDArray[np.float64]]:
131138
"""Calculate GC skew in sliding window
132139
133140
Parameters
@@ -141,7 +148,7 @@ def calc_gc_skew(
141148
142149
Returns
143150
-------
144-
gc_skew_result_tuple : tuple[np.ndarray, np.ndarray]
151+
gc_skew_result_tuple : tuple[NDArray[np.int64], NDArray[np.float64]]
145152
Position list & GC skew list
146153
"""
147154
pos_list, gc_skew_list = [], []
@@ -168,15 +175,18 @@ def calc_gc_skew(
168175
skew = 0.0
169176
gc_skew_list.append(skew)
170177

171-
return (np.array(pos_list), np.array(gc_skew_list))
178+
pos_list = np.array(pos_list).astype(np.int64)
179+
gc_skew_list = np.array(gc_skew_list).astype(np.float64)
180+
181+
return pos_list, gc_skew_list
172182

173183
def calc_gc_content(
174184
self,
175185
window_size: int | None = None,
176186
step_size: int | None = None,
177187
*,
178188
seq: str | None = None,
179-
) -> tuple[np.ndarray, np.ndarray]:
189+
) -> tuple[NDArray[np.int64], NDArray[np.float64]]:
180190
"""Calculate GC content in sliding window
181191
182192
Parameters
@@ -190,7 +200,7 @@ def calc_gc_content(
190200
191201
Returns
192202
-------
193-
gc_content_result_tuple : tuple[np.ndarray, np.ndarray]
203+
gc_content_result_tuple : tuple[NDArray[np.int64], NDArray[np.float64]]
194204
Position list & GC content list
195205
"""
196206
pos_list, gc_content_list = [], []
@@ -212,7 +222,10 @@ def calc_gc_content(
212222
gc_content = SeqUtils.gc_fraction(subseq) * 100
213223
gc_content_list.append(gc_content)
214224

215-
return (np.array(pos_list), np.array(gc_content_list))
225+
pos_list = np.array(pos_list).astype(np.int64)
226+
gc_content_list = np.array(gc_content_list).astype(np.float64)
227+
228+
return pos_list, gc_content_list
216229

217230
def get_seqid2seq(self) -> dict[str, str]:
218231
"""Get seqid & complete/contig/scaffold genome sequence dict
@@ -236,14 +249,14 @@ def get_seqid2size(self) -> dict[str, int]:
236249

237250
def get_seqid2features(
238251
self,
239-
feature_type: str | None = "CDS",
252+
feature_type: str | list[str] | None = "CDS",
240253
target_strand: int | None = None,
241254
) -> dict[str, list[SeqFeature]]:
242255
"""Get seqid & features in target seqid genome dict
243256
244257
Parameters
245258
----------
246-
feature_type : str | None, optional
259+
feature_type : str | list[str] | None, optional
247260
Feature type (`CDS`, `gene`, `mRNA`, etc...)
248261
If None, extract regardless of feature type.
249262
target_strand : int | None, optional
@@ -254,12 +267,15 @@ def get_seqid2features(
254267
seqid2features : dict[str, list[SeqFeature]]
255268
seqid & features dict
256269
"""
270+
if isinstance(feature_type, str):
271+
feature_type = [feature_type]
272+
257273
seqid2features = defaultdict(list)
258274
for rec in self.records:
259275
feature: SeqFeature
260276
for feature in rec.features:
261277
strand = feature.location.strand
262-
if feature_type is not None and feature.type != feature_type:
278+
if feature_type is not None and feature.type not in feature_type:
263279
continue
264280
if target_strand is not None and strand != target_strand:
265281
continue
@@ -279,15 +295,16 @@ def get_seqid2features(
279295

280296
def extract_features(
281297
self,
282-
feature_type: str | None = "CDS",
298+
feature_type: str | list[str] | None = "CDS",
299+
*,
283300
target_strand: int | None = None,
284301
target_range: tuple[int, int] | None = None,
285302
) -> list[SeqFeature]:
286303
"""Extract features (only first record)
287304
288305
Parameters
289306
----------
290-
feature_type : str | None, optional
307+
feature_type : str | list[str] | None, optional
291308
Feature type (`CDS`, `gene`, `mRNA`, etc...)
292309
If None, extract regardless of feature type.
293310
target_strand : int | None, optional

0 commit comments

Comments
 (0)