Skip to content

Commit

Permalink
Merge pull request #68 from moshi4/develop
Browse files Browse the repository at this point in the history
Bump to v1.5.0
  • Loading branch information
moshi4 authored May 18, 2024
2 parents 45ad937 + a2138d2 commit bf74abb
Show file tree
Hide file tree
Showing 12 changed files with 835 additions and 711 deletions.
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
# See https://pre-commit.com/hooks.html for more hooks
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.3.1
rev: v0.4.4
hooks:
- id: ruff
name: ruff lint check
Expand Down
77 changes: 77 additions & 0 deletions docs/radar_chart.ipynb

Large diffs are not rendered by default.

1,262 changes: 623 additions & 639 deletions poetry.lock

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "pyCirclize"
version = "1.4.0"
version = "1.5.0"
description = "Circular visualization in Python"
authors = ["moshi4"]
license = "MIT"
Expand Down
2 changes: 1 addition & 1 deletion src/pycirclize/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from pycirclize.circos import Circos

__version__ = "1.4.0"
__version__ = "1.5.0"

__all__ = [
"Circos",
Expand Down
66 changes: 44 additions & 22 deletions src/pycirclize/circos.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,21 +40,20 @@ class Circos:

def __init__(
self,
sectors: Mapping[str, int | float],
sectors: Mapping[str, int | float | tuple[float, float]],
start: float = 0,
end: float = 360,
*,
space: float | list[float] = 0,
endspace: bool = True,
sector2start_pos: Mapping[str, int | float] | None = None,
sector2clockwise: dict[str, bool] | None = None,
show_axis_for_debug: bool = False,
):
"""
Parameters
----------
sectors : Mapping[str, int | float]
Sector name & size dict
sectors : Mapping[str, int | float | tuple[float, float]]
Sector name & size (or range) dict
start : float, optional
Plot start degree (`-360 <= start < end <= 360`)
end : float, optional
Expand All @@ -63,14 +62,11 @@ def __init__(
Space degree(s) between sector
endspace : bool, optional
If True, insert space after the end sector
sector2start_pos : Mapping[str, int | float] | None, optional
Sector name & start position dict. By default, `start_pos=0`.
sector2clockwise : dict[str, bool] | None, optional
Sector name & clockwise bool dict. By default, `clockwise=True`.
show_axis_for_debug : bool, optional
Show axis for position check debugging (Developer option)
"""
sector2start_pos = {} if sector2start_pos is None else sector2start_pos
sector2clockwise = {} if sector2clockwise is None else sector2clockwise

# Check start-end degree range
Expand Down Expand Up @@ -100,19 +96,21 @@ def __init__(
"""
)[1:-1]
raise ValueError(err_msg)
sector_total_size = sum(sectors.values())

sector2range = self._to_sector2range(sectors)
sector_total_size = sum([max(r) - min(r) for r in sector2range.values()])

rad_pos = math.radians(start)
self._sectors: list[Sector] = []
for idx, (sector_name, sector_size) in enumerate(sectors.items()):
for idx, (sector_name, sector_range) in enumerate(sector2range.items()):
sector_size = max(sector_range) - min(sector_range)
sector_size_ratio = sector_size / sector_total_size
deg_size = whole_deg_size_without_space * sector_size_ratio
rad_size = math.radians(deg_size)
rad_lim = (rad_pos, rad_pos + rad_size)
rad_pos += rad_size + math.radians(space_list[idx])
start_pos = sector2start_pos.get(sector_name, 0)
clockwise = sector2clockwise.get(sector_name, True)
sector = Sector(sector_name, sector_size, rad_lim, start_pos, clockwise)
sector = Sector(sector_name, sector_range, rad_lim, clockwise)
self._sectors.append(sector)

self._deg_lim = (start, end)
Expand Down Expand Up @@ -180,6 +178,7 @@ def radar_chart(
table: str | Path | pd.DataFrame | RadarTable,
*,
r_lim: tuple[float, float] = (0, 100),
vmin: float = 0,
vmax: float = 100,
fill: bool = True,
marker_size: int = 0,
Expand All @@ -203,6 +202,8 @@ def radar_chart(
Table file or Table dataframe or RadarTable instance
r_lim : tuple[float, float], optional
Radar chart radius limit region (0 - 100)
vmin : float, optional
Min value
vmax : float, optional
Max value
fill : bool, optional
Expand Down Expand Up @@ -244,6 +245,10 @@ def radar_chart(
circos : Circos
Circos instance initialized for radar chart
"""
if not vmin < vmax:
raise ValueError(f"vmax must be larger than vmin ({vmin=}, {vmax=})")
size = vmax - vmin

# Setup default properties
grid_line_kws = {} if grid_line_kws is None else deepcopy(grid_line_kws)
for k, v in dict(color="grey", ls="dashed", lw=0.5).items():
Expand All @@ -269,11 +274,12 @@ def radar_chart(
if not 0 < grid_interval_ratio <= 1.0:
raise ValueError(f"{grid_interval_ratio=} is invalid.")
# Plot horizontal grid line & label
stop, step = vmax + (vmax / 1000), vmax * grid_interval_ratio
for v in np.arange(0, stop, step):
track.line(x, [v] * len(x), vmax=vmax, arc=circular, **grid_line_kws)
stop, step = vmax + (size / 1000), size * grid_interval_ratio
for v in np.arange(vmin, stop, step):
y = [v] * len(x)
track.line(x, y, vmin=vmin, vmax=vmax, arc=circular, **grid_line_kws)
if show_grid_label:
r = track._y_to_r(v, 0, vmax)
r = track._y_to_r(v, vmin, vmax)
# Format grid label
if grid_label_formatter:
text = grid_label_formatter(v)
Expand All @@ -283,7 +289,7 @@ def radar_chart(
track.text(text, 0, r, **grid_label_kws)
# Plot vertical grid line
for p in x[:-1]:
track.line([p, p], [0, vmax], vmax=vmax, **grid_line_kws)
track.line([p, p], [vmin, vmax], vmin=vmin, vmax=vmax, **grid_line_kws)

# Plot radar charts
if isinstance(cmap, str):
Expand All @@ -296,15 +302,16 @@ def radar_chart(
line_kws = line_kws_handler(row_name) if line_kws_handler else {}
line_kws.setdefault("lw", 1.0)
line_kws.setdefault("label", row_name)
track.line(x, y, vmax=vmax, arc=False, color=color, **line_kws)
track.line(x, y, vmin=vmin, vmax=vmax, arc=False, color=color, **line_kws)
if marker_size > 0:
marker_kws = marker_kws_handler(row_name) if marker_kws_handler else {}
marker_kws.setdefault("marker", "o")
marker_kws.setdefault("zorder", 2)
marker_kws.update(s=marker_size**2)
track.scatter(x, y, vmax=vmax, color=color, **marker_kws)
track.scatter(x, y, vmin=vmin, vmax=vmax, color=color, **marker_kws)
if fill:
track.fill_between(x, y, vmax=vmax, arc=False, color=color, alpha=0.5)
fill_kws = dict(arc=False, color=color, alpha=0.5)
track.fill_between(x, y, y2=vmin, vmin=vmin, vmax=vmax, **fill_kws) # type:ignore

# Plot column names
for idx, col_name in enumerate(radar_table.col_names):
Expand Down Expand Up @@ -577,15 +584,13 @@ def initialize_from_bed(
Circos instance initialized from BED file
"""
records = Bed(bed_file).records
sectors = {rec.chr: rec.size for rec in records}
sector2start_pos = {rec.chr: rec.start for rec in records}
sectors = {rec.chr: (rec.start, rec.end) for rec in records}
return Circos(
sectors,
start,
end,
space=space,
endspace=endspace,
sector2start_pos=sector2start_pos,
sector2clockwise=sector2clockwise,
)

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

def _to_sector2range(
self,
sectors: Mapping[str, int | float | tuple[float, float]],
) -> dict[str, tuple[float, float]]:
"""Convert sectors to sector2range"""
sector2range: dict[str, tuple[float, float]] = {}
for name, value in sectors.items():
if isinstance(value, (tuple, list)):
sector_start, sector_end = value
if not sector_start < sector_end:
err_msg = f"{sector_end=} must be larger than {sector_start=}."
raise ValueError(err_msg)
sector2range[name] = (sector_start, sector_end)
else:
sector2range[name] = (0, value)
return sector2range

def _initialize_figure(
self,
figsize: tuple[float, float] = (8, 8),
Expand Down
41 changes: 29 additions & 12 deletions src/pycirclize/parser/genbank.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,16 @@
from collections import defaultdict
from io import StringIO, TextIOWrapper
from pathlib import Path
from typing import TYPE_CHECKING

import numpy as np
from Bio import SeqIO, SeqUtils
from Bio.SeqFeature import Seq, SeqFeature, SimpleLocation
from Bio.SeqRecord import SeqRecord

if TYPE_CHECKING:
from numpy.typing import NDArray


class Genbank:
"""Genbank Parser Class"""
Expand Down Expand Up @@ -56,11 +60,14 @@ def __init__(
elif isinstance(self._gbk_source, (StringIO, TextIOWrapper)):
self._name = self._records[0].name
else:
raise NotImplementedError("Failed to get name.")
raise ValueError("Failed to get genbank name.")

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

if len(self.records) == 0:
raise ValueError(f"Failed to parse {gbk_source} as Genbank file.")

############################################################
# Property
############################################################
Expand Down Expand Up @@ -127,7 +134,7 @@ def calc_gc_skew(
step_size: int | None = None,
*,
seq: str | None = None,
) -> tuple[np.ndarray, np.ndarray]:
) -> tuple[NDArray[np.int64], NDArray[np.float64]]:
"""Calculate GC skew in sliding window
Parameters
Expand All @@ -141,7 +148,7 @@ def calc_gc_skew(
Returns
-------
gc_skew_result_tuple : tuple[np.ndarray, np.ndarray]
gc_skew_result_tuple : tuple[NDArray[np.int64], NDArray[np.float64]]
Position list & GC skew list
"""
pos_list, gc_skew_list = [], []
Expand All @@ -168,15 +175,18 @@ def calc_gc_skew(
skew = 0.0
gc_skew_list.append(skew)

return (np.array(pos_list), np.array(gc_skew_list))
pos_list = np.array(pos_list).astype(np.int64)
gc_skew_list = np.array(gc_skew_list).astype(np.float64)

return pos_list, gc_skew_list

def calc_gc_content(
self,
window_size: int | None = None,
step_size: int | None = None,
*,
seq: str | None = None,
) -> tuple[np.ndarray, np.ndarray]:
) -> tuple[NDArray[np.int64], NDArray[np.float64]]:
"""Calculate GC content in sliding window
Parameters
Expand All @@ -190,7 +200,7 @@ def calc_gc_content(
Returns
-------
gc_content_result_tuple : tuple[np.ndarray, np.ndarray]
gc_content_result_tuple : tuple[NDArray[np.int64], NDArray[np.float64]]
Position list & GC content list
"""
pos_list, gc_content_list = [], []
Expand All @@ -212,7 +222,10 @@ def calc_gc_content(
gc_content = SeqUtils.gc_fraction(subseq) * 100
gc_content_list.append(gc_content)

return (np.array(pos_list), np.array(gc_content_list))
pos_list = np.array(pos_list).astype(np.int64)
gc_content_list = np.array(gc_content_list).astype(np.float64)

return pos_list, gc_content_list

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

def get_seqid2features(
self,
feature_type: str | None = "CDS",
feature_type: str | list[str] | None = "CDS",
target_strand: int | None = None,
) -> dict[str, list[SeqFeature]]:
"""Get seqid & features in target seqid genome dict
Parameters
----------
feature_type : str | None, optional
feature_type : str | list[str] | None, optional
Feature type (`CDS`, `gene`, `mRNA`, etc...)
If None, extract regardless of feature type.
target_strand : int | None, optional
Expand All @@ -254,12 +267,15 @@ def get_seqid2features(
seqid2features : dict[str, list[SeqFeature]]
seqid & features dict
"""
if isinstance(feature_type, str):
feature_type = [feature_type]

seqid2features = defaultdict(list)
for rec in self.records:
feature: SeqFeature
for feature in rec.features:
strand = feature.location.strand
if feature_type is not None and feature.type != feature_type:
if feature_type is not None and feature.type not in feature_type:
continue
if target_strand is not None and strand != target_strand:
continue
Expand All @@ -279,15 +295,16 @@ def get_seqid2features(

def extract_features(
self,
feature_type: str | None = "CDS",
feature_type: str | list[str] | None = "CDS",
*,
target_strand: int | None = None,
target_range: tuple[int, int] | None = None,
) -> list[SeqFeature]:
"""Extract features (only first record)
Parameters
----------
feature_type : str | None, optional
feature_type : str | list[str] | None, optional
Feature type (`CDS`, `gene`, `mRNA`, etc...)
If None, extract regardless of feature type.
target_strand : int | None, optional
Expand Down
Loading

0 comments on commit bf74abb

Please sign in to comment.