Skip to content

Commit

Permalink
Merge pull request #63 from moshi4/develop
Browse files Browse the repository at this point in the history
Bump to v1.4.0
  • Loading branch information
moshi4 authored Apr 1, 2024
2 parents 4a3c82a + b547b5e commit 45ad937
Show file tree
Hide file tree
Showing 22 changed files with 1,879 additions and 1,225 deletions.
8 changes: 4 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,11 @@ jobs:
- name: Install dependencies
run: pip install -e . pytest pytest-cov ruff

- name: Run ruff format check
run: ruff format --check --diff

- name: Run ruff lint check
run: ruff check --diff

- name: Run ruff format check
run: ruff format --check --diff

- name: Run pytest
run: pytest tests --tb=line --cov=src --cov-report=xml --cov-report=term
run: pytest
11 changes: 11 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# See https://pre-commit.com for more information
# See https://pre-commit.com/hooks.html for more hooks
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.3.1
hooks:
- id: ruff
name: ruff lint check
args: [--fix]
- id: ruff-format
name: ruff format check
41 changes: 38 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ API usage is described in each of the following sections in the [document](https
- [Getting Started](https://moshi4.github.io/pyCirclize/getting_started/)
- [Plot API Example](https://moshi4.github.io/pyCirclize/plot_api_example/)
- [Chord Diagram](https://moshi4.github.io/pyCirclize/chord_diagram/)
- [Radar Chart](https://moshi4.github.io/pyCirclize/radar_chart/)
- [Circos Plot (Genomics)](https://moshi4.github.io/pyCirclize/circos_plot/)
- [Phylogenetic Tree](https://moshi4.github.io/pyCirclize/phylogenetic_tree/)
- [Plot Tips](https://moshi4.github.io/pyCirclize/plot_tips/)
Expand Down Expand Up @@ -125,8 +126,8 @@ r_cds_track.genomic_features(r_cds_feats, plotstyle="arrow", fc="skyblue", lw=0.
# Plot 'gene' qualifier label if exists
labels, label_pos_list = [], []
for feat in gbk.extract_features("CDS"):
start = int(str(feat.location.start))
end = int(str(feat.location.end))
start = int(feat.location.start)
end = int(feat.location.end)
label_pos = (start + end) / 2
gene_name = feat.qualifiers.get("gene", [None])[0]
if gene_name is not None:
Expand Down Expand Up @@ -223,6 +224,39 @@ fig.savefig("example04.png")

![example04.png](https://raw.githubusercontent.com/moshi4/pyCirclize/main/docs/images/example04.png)

### 5. Radar Chart

```python
from pycirclize import Circos
import pandas as pd

# Create RPG jobs parameter dataframe (3 jobs, 7 parameters)
df = pd.DataFrame(
data=[
[80, 80, 80, 80, 80, 80, 80],
[90, 20, 95, 95, 30, 30, 80],
[60, 90, 20, 20, 100, 90, 50],
],
index=["Hero", "Warrior", "Wizard"],
columns=["HP", "MP", "ATK", "DEF", "SP.ATK", "SP.DEF", "SPD"],
)

# Initialize Circos instance for radar chart plot
circos = Circos.radar_chart(
df,
vmax=100,
marker_size=6,
grid_interval_ratio=0.2,
)

# Plot figure & set legend on upper right
fig = circos.plotfig()
_ = circos.ax.legend(loc="upper right", fontsize=10)
fig.savefig("example05.png")
```

![example05.png](https://raw.githubusercontent.com/moshi4/pyCirclize/main/docs/images/example05.png)

## Not Implemented Features

List of features implemented in other Circos plotting tools but not yet implemented in pyCirclize.
Expand All @@ -231,7 +265,8 @@ I may implement them when I feel like it.
- Plot histogram
- Plot boxplot
- Plot violin
- Label position auto adjustment
- Plot curved text
- Adjust overlap label position

## Star History

Expand Down
1 change: 1 addition & 0 deletions docs/getting_started.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -502,6 +502,7 @@
"If you are interested in the contents of the following sections, you may want to look at them next.\n",
"\n",
"- [Chord Diagram](../chord_diagram/)\n",
"- [Radar Chart](../radar_chart/)\n",
"- [Circos Plot (Genomics)](../circos_plot/)\n",
"- [Phylogenetic Tree](../phylogenetic_tree/)"
]
Expand Down
Binary file added docs/images/example05.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ API usage is described in each of the following sections.
- [Getting Started](./getting_started/)
- [Plot API Example](./plot_api_example/)
- [Chord Diagram](./chord_diagram/)
- [Radar Chart](./radar_chart/)
- [Circos Plot (Genomics)](./circos_plot/)
- [Phylogenetic Tree](./phylogenetic_tree/)
- [Plot Tips](./plot_tips/)
243 changes: 243 additions & 0 deletions docs/radar_chart.ipynb

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ nav:
- Getting Started: getting_started.ipynb
- Plot API Example: plot_api_example.ipynb
- Chord Diagram: chord_diagram.ipynb
- Radar Chart: radar_chart.ipynb
- Circos Plot (Genomics): circos_plot.ipynb
- Phylogenetic Tree: phylogenetic_tree.ipynb
- Plot Tips: plot_tips.ipynb
Expand Down
1,661 changes: 923 additions & 738 deletions poetry.lock

Large diffs are not rendered by default.

7 changes: 4 additions & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "pyCirclize"
version = "1.3.0"
version = "1.4.0"
description = "Circular visualization in Python"
authors = ["moshi4"]
license = "MIT"
Expand All @@ -23,7 +23,7 @@ include = ["tests"]

[tool.pytest.ini_options]
minversion = "6.0"
addopts = "--cov=src --tb=line --cov-report=xml --cov-report=term"
addopts = "--cov=src --tb=long -vv --cov-report=xml --cov-report=term"
testpaths = ["tests"]

[tool.ruff]
Expand Down Expand Up @@ -57,12 +57,13 @@ convention = "numpy"
[tool.poetry.dependencies]
python = "^3.8"
matplotlib = ">=3.5.2"
biopython = ">=1.79"
biopython = ">=1.80"
numpy = ">=1.21.1"
pandas = ">=1.3.5"

[tool.poetry.group.dev.dependencies]
ruff = ">=0.1.6"
pre-commit = ">=3.5.0"
pytest = ">=7.1.2"
pytest-cov = ">=4.0.0"
ipykernel = ">=6.13.0"
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.3.0"
__version__ = "1.4.0"

__all__ = [
"Circos",
Expand Down
163 changes: 160 additions & 3 deletions src/pycirclize/circos.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from typing import Any, Callable

import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
from Bio.Phylo.BaseTree import Tree
from matplotlib.axes import Axes
Expand All @@ -21,7 +22,7 @@
from matplotlib.projections.polar import PolarAxes

from pycirclize import config, utils
from pycirclize.parser import Bed, Matrix
from pycirclize.parser import Bed, Matrix, RadarTable
from pycirclize.patches import (
ArcLine,
ArcRectangle,
Expand Down Expand Up @@ -174,6 +175,154 @@ def ax(self) -> PolarAxes:
# Public Method
############################################################

@staticmethod
def radar_chart(
table: str | Path | pd.DataFrame | RadarTable,
*,
r_lim: tuple[float, float] = (0, 100),
vmax: float = 100,
fill: bool = True,
marker_size: int = 0,
bg_color: str | None = "#eeeeee80",
circular: bool = False,
cmap: str | dict[str, str] = "Set2",
show_grid_label: bool = True,
grid_interval_ratio: float | None = 0.2,
grid_line_kws: dict[str, Any] | None = None,
grid_label_kws: dict[str, Any] | None = None,
grid_label_formatter: Callable[[float], str] | None = None,
label_kws_handler: Callable[[str], dict[str, Any]] | None = None,
line_kws_handler: Callable[[str], dict[str, Any]] | None = None,
marker_kws_handler: Callable[[str], dict[str, Any]] | None = None,
) -> Circos:
"""Plot radar chart
Parameters
----------
table : str | Path | pd.DataFrame | RadarTable
Table file or Table dataframe or RadarTable instance
r_lim : tuple[float, float], optional
Radar chart radius limit region (0 - 100)
vmax : float, optional
Max value
fill : bool, optional
If True, fill color of radar chart.
marker_size : int, optional
Marker size
bg_color : str | None, optional
Background color
circular : bool, optional
If True, plot with circular style.
cmap : str | dict[str, str], optional
Colormap assigned to each target row(index) in table.
User can set matplotlib's colormap (e.g. `tab10`, `Set2`) or
target_name -> color dict (e.g. `dict(A="red", B="blue", C="green", ...)`)
show_grid_label : bool, optional
If True, show grid label.
grid_interval_ratio : float | None, optional
Grid interval ratio (0.0 - 1.0)
grid_line_kws : dict[str, Any] | None, optional
Keyword arguments passed to `track.line()` method
(e.g. `dict(color="black", ls="dotted", lw=1.0, ...)`)
grid_label_kws : dict[str, Any] | None, optional
Keyword arguments passed to `track.text()` method
(e.g. `dict(size=12, color="red", ...)`)
grid_label_formatter : Callable[[float], str] | None, optional
User-defined function to format grid label (e.g. `lambda v: f"{v:.1f}%"`).
label_kws_handler : Callable[[str], dict[str, Any]] | None, optional
Handler function for keyword arguments passed to `track.text()` method.
Handler function takes each column name of table as an argument.
line_kws_handler : Callable[[str], dict[str, Any]] | None, optional
Handler function for keyword arguments passed to `track.line()` method.
Handler function takes each row(index) name of table as an argument.
marker_kws_handler : Callable[[str], dict[str, Any]] | None, optional
Handler function for keyword arguments passed to `track.scatter()` method.
Handler function takes each row(index) name of table as an argument.
Returns
-------
circos : Circos
Circos instance initialized for radar chart
"""
# 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():
grid_line_kws.setdefault(k, v)

grid_label_kws = {} if grid_label_kws is None else deepcopy(grid_label_kws)
for k, v in dict(color="dimgrey", size=10, ha="left", va="top").items():
grid_label_kws.setdefault(k, v)

# Initialize circos for radar chart
radar_table = table if isinstance(table, RadarTable) else RadarTable(table)
circos = Circos(dict(radar=radar_table.col_num))
sector = circos.sectors[0]
track = sector.add_track(r_lim)
x = np.arange(radar_table.col_num + 1)

# Plot background color
if bg_color:
track.fill_between(x, [vmax] * len(x), arc=circular, color=bg_color)

# Plot grid line
if grid_interval_ratio:
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)
if show_grid_label:
r = track._y_to_r(v, 0, vmax)
# Format grid label
if grid_label_formatter:
text = grid_label_formatter(v)
else:
v = float(f"{v:.9f}") # Correct rounding error
text = f"{v:.0f}" if math.isclose(int(v), float(v)) else str(v)
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)

# Plot radar charts
if isinstance(cmap, str):
row_name2color = radar_table.get_row_name2color(cmap)
else:
row_name2color = cmap
for row_name, values in radar_table.row_name2values.items():
y = values + [values[0]]
color = row_name2color[row_name]
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)
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)
if fill:
track.fill_between(x, y, vmax=vmax, arc=False, color=color, alpha=0.5)

# Plot column names
for idx, col_name in enumerate(radar_table.col_names):
deg = 360 * (idx / sector.size)
label_kws = label_kws_handler(col_name) if label_kws_handler else {}
label_kws.setdefault("size", 12)
if math.isclose(deg, 0):
label_kws.update(va="bottom")
elif math.isclose(deg, 180):
label_kws.update(va="top")
elif 0 < deg < 180:
label_kws.update(ha="left")
elif 180 < deg < 360:
label_kws.update(ha="right")
track.text(col_name, idx, r=105, adjust_rotation=False, **label_kws)

return circos

@staticmethod
def initialize_from_matrix(
matrix: str | Path | pd.DataFrame | Matrix,
Expand Down Expand Up @@ -302,6 +451,8 @@ def initialize_from_matrix(

return circos

chord_diagram = initialize_from_matrix

@staticmethod
def initialize_from_tree(
tree_data: str | Path | Tree,
Expand Down Expand Up @@ -837,6 +988,7 @@ def plotfig(
dpi: int = 100,
*,
ax: PolarAxes | None = None,
figsize: tuple[float, float] = (8, 8),
) -> Figure:
"""Plot figure
Expand All @@ -846,6 +998,8 @@ def plotfig(
Figure DPI
ax : PolarAxes | None
If None, figure and axes are newly created.
figsize : tuple[float, float], optional
Figure size
Returns
-------
Expand All @@ -854,7 +1008,7 @@ def plotfig(
"""
if ax is None:
# Initialize Figure & PolarAxes
fig, ax = self._initialize_figure(dpi=dpi)
fig, ax = self._initialize_figure(figsize=figsize, dpi=dpi)
else:
# Check PolarAxes or not
if not isinstance(ax, PolarAxes):
Expand Down Expand Up @@ -891,6 +1045,7 @@ def savefig(
savefile: str | Path,
*,
dpi: int = 100,
figsize: tuple[float, float] = (8, 8),
pad_inches: float = 0.5,
) -> None:
"""Save figure to file
Expand All @@ -904,10 +1059,12 @@ def savefig(
Save file (`*.png`|`*.jpg`|`*.svg`|`*.pdf`)
dpi : int, optional
DPI
figsize : tuple[float, float], optional
Figure size
pad_inches : float, optional
Padding inches
"""
fig = self.plotfig(dpi=dpi)
fig = self.plotfig(dpi=dpi, figsize=figsize)
fig.savefig(
fname=savefile, # type: ignore
dpi=dpi,
Expand Down
Loading

0 comments on commit 45ad937

Please sign in to comment.