Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bump to v1.5.0 #68

Merged
merged 9 commits into from
May 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading