Library / SDK
moshi4/pyCirclize avatar
moshi4/pyCirclize

pyCirclize: circular plots and genomic tracks built on matplotlib

Circular visualization in Python (Circos Plot, Chord Diagram, Radar Chart)

1,127 stars65 forksPythonMIT

At a glance

What is it?
pyCirclize is a Python package for circular visualisation that sits directly on matplotlib rather than reimplementing a drawing layer, and it adds the genomics side that the Circos style of plot usually implies: GenBank parsing, forward and reverse strand CDS tracks and phylogenetic trees drawn as circles. The current release is v1.10.1 and it needs Python 3.10 or newer.
Who is it for?
pyCirclize fits a bioinformatician who needs a Circos style figure for a genome comparison, a comparative genomics view or a circular tree, and who wants the output to be a matplotlib figure they can compose with other plots. It is the wrong tool if you need a highly custom diagram outside the sector and track model, since the geometry is built around radial layout, and the wrong tool if you want interactive output rather than a static image.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 131 days ago.
What is it written in?
Mainly Python, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.

Editorial analysis

The design: matplotlib underneath, a radial model on top

pyCirclize is a circular visualization Python package implemented based on matplotlib, developed for the purpose of easily and beautifully plotting circular figures such as a Circos plot and a chord diagram. It is also where useful genome and phylogenetic tree visualisation methods for the bioinformatics field are implemented.

The decision to sit on matplotlib rather than implement a drawing layer is the foundation of the whole library, and it has consequences in both directions.

On the good side, everything matplotlib can do is available, because a pyCirclize figure is a matplotlib figure. You can compose it with other axes, save it in any format matplotlib supports, and use the whole matplotlib styling and theming ecosystem. The pyproject.toml even lists Framework:: Matplotlib as a classifier, and the runtime dependencies are biopython, matplotlib, numpy and pandas, which is a very short dependency list for a plotting library because matplotlib is doing the drawing.

On the other side, the geometry is constrained to a radial model. A Circos plot is a circle of sectors, and everything in this library is a track within a sector, an axis, a link between sectors, or a tree drawn in that space. The library is not a general diagram engine, and the API reflects that, because the primitives are radial ones.

The lineage is declared: pyCirclize was inspired by circlize and pyCircos. Both of those are also Circos style tools, so this is a deliberate reimplementation of a specific visual grammar rather than a general purpose plotting library with a circle helper.

The domains it serves are stated in the topic tags, and they are unusually specific: bioinformatics, comparative genomics, genomics visualization, microbial genomics and phylogenetic trees. This is a library for a particular scientific audience, and the genomic features are not an add-on, they are half the reason it exists.

The licence is MIT, the primary language is Python, the homepage is moshi4.github.io/pyCirclize, and the recent releases are v1.10.1 on 2025-10-03, v1.10.0 on 2025-08-23 and v1.9.1 on 2025-04-18. The repository is not archived, with the last push on 2026-06-14.

The API, and what a sector and a track actually are

The Circos model in this library is two objects: sectors and tracks. Understanding them is enough to read any of the examples.

A sector is a named slice of the circle, and it owns the data for that slice. The first example in the README creates five of them from a plain dictionary of name to size:

python
from pycirclize import Circos
import numpy as np
np.random.seed(0)

# Initialize Circos sectors
sectors = {"A": 10, "B": 15, "C": 12, "D": 20, "E": 15}
circos = Circos(sectors, space=5)

for sector in circos.sectors:
    # Plot sector name
    sector.text(f"Sector: {sector.name}", r=110, size=15)
    # Create x positions & random y values
    x = np.arange(sector.start, sector.end) + 0.5
    y = np.random.randint(0, 100, len(x))
    # Plot lines
    track1 = sector.add_track((80, 100), r_pad_ratio=0.1)
    track1.xticks_by_interval(interval=1)
    track1.axis()
    track1.line(x, y)

The key idea is that a track is a radial band, and it is created with a pair of radii. The track here spans 80 to 100, the next one spans 55 to 75, the third 30 to 50, so you stack concentric bands by giving each a different radius range. r_pad_ratio adds proportional padding between them, and a track carries the plotting methods you would expect, line, scatter, bar, axis for the frame, and xticks_by_interval for a tick scale.

The coordinates are data coordinates within the sector, which is why x is computed as np.arange(sector.start, sector.end) + 0.5. sector.start and sector.end are the boundaries of the sector in the circle's own units, so adding 0.5 puts each data point in the middle of its unit rather than on the boundary. That is the same convention as a genome coordinate system, and it is deliberate.

The links are the other primitive, and they connect two positions on the circle. The link call takes a triple of sector name and start and end, and there are visual options for colour, edge colour, line width, hatch pattern and direction:

python
circos.link(("A", 0, 3), ("B", 15, 12))
circos.link(("B", 0, 3), ("C", 7, 11), color="skyblue")
circos.link(("C", 2, 5), ("E", 15, 12), color="chocolate", direction=1)

A link with direction set is drawn as a ribbon rather than a straight chord, which is what makes the chord diagram variant readable.

And the output is a matplotlib save, circos.savefig("example01.png"), which is the last line of the example and the point of the whole design.

The genomic track: GenBank, CDS features and strand separation

The second example is the reason the library exists, and it is worth reading in full because it shows the genomics workflow.

python
from pycirclize import Circos
from pycirclize.utils import fetch_genbank_by_accid
from pycirclize.parser import Genbank

# Download `NC_002483` E.coli plasmid genbank
gbk_fetch_data = fetch_genbank_by_accid("NC_002483")
gbk = Genbank(gbk_fetch_data)

# Initialize Circos instance with genome size
sectors = gbk.get_seqid2size()
space = 0 if len(sectors) == 1 else 2
circos = Circos(sectors, space=space)
circos.text(f"Escherichia coli K-12 plasmid F\n\n{gbk.name}", size=14)

seqid2features = gbk.get_seqid2features(feature_type="CDS")

The flow is straightforward. A GenBank record is fetched by accession, parsed into a Genbank object, and the object gives you a mapping of sequence id to size, which becomes the sectors. The space value is set conditionally, zero when there is a single sequence and two when there are several, which is a small thing that shows the library is thinking about the visual result rather than just the data.

The CDS features are then split by strand, and each strand gets its own track:

python
for sector in circos.sectors:
    # Setup track for features plot
    f_cds_track = sector.add_track((95, 100))
    f_cds_track.axis(fc="lightgrey", ec="none", alpha=0.5)
    r_cds_track = sector.add_track((90, 95))
    r_cds_track.axis(fc="lightgrey", ec="none", alpha=0.5)
    # Plot forward/reverse strand CDS
    features = seqid2features[sector.name]
    for feature in features:
        if feature.location.strand == 1:
            f_cds_track.genomic_features(feature, plotstyle="arrow", fc="salmon", lw=0.5)
        else:
            r_cds_track.genomic_features(feature, plotstyle="arrow", fc="skyblue", lw=0.5)

Two tracks, one for the forward strand at radius 95 to 100 and one for the reverse at 90 to 95, and the feature is drawn on the appropriate one according to its strand. The genomic_features method takes a Biopython feature and a plot style, here arrow, with a fill colour and a line width. The two tracks are also given a faint grey axis so the reader can see the genomic coordinate frame behind the features.

The gene name labels come from the feature qualifiers, and the position is the midpoint of the feature:

python
labels, label_pos_list = [], []
for feature in features:
    start = int(feature.location.start)
    end = int(feature.location.end)
    label_pos = (start + end) / 2
    gene_name = feature.qualifiers.get("gene", [None])[0]
    if gene_name is not None:
        labels.append(gene_name)
        label_pos_list.append(label_pos)
        f_cds_track.annotate(label_pos, gene_name, label_size=6)

The qualifier lookup is written defensively, with a default of [None] and a check, because a feature without a gene qualifier is normal and a KeyError there would lose the whole figure. The tick scale is then set in kilobases:

python
r_cds_track.xticks_by_interval(
    10000, outer=False, label_formatter=lambda v: f"{v/1000:.1f} Kb"
)

Note that the formatter divides by a thousand, so a tick every ten thousand bases reads as kilobases. That is the small piece of customisation that turns a generic coordinate axis into a genome scale.

Chord diagrams from a matrix, and trees as circles

The chord diagram example is the shortest of the set, because the class method does the work.

python
from pycirclize import Circos
import pandas as pd

# Create matrix dataframe (3 x 6)
row_names = ["F1", "F2", "F3"]
col_names = ["T1", "T2", "T3", "T4", "T5", "T6"]
matrix_data = [
    [10, 16, 7, 7, 10, 8],
    [4, 9, 10, 12, 12, 7],
    [17, 13, 7, 4, 20, 4],
]
matrix_df = pd.DataFrame(matrix_data, index=row_names, columns=col_names)

# Initialize Circos instance for chord diagram plot
circos = Circos.chord_diagram(
    matrix_df,
    space=5,
    cmap="tab10",
    label_kws=dict(size=12),
    link_kws=dict(ec="black", lw=0.5, direction=1),
)

circos.savefig("example03.png")

The input is a pandas DataFrame with row and column names, and the output is a full Circos figure. The keyword arguments pass through to the two halves of the picture, label_kws for the sector labels and link_kws for the ribbons, and cmap is a matplotlib colormap, tab10 here. So the chord diagram is not a separate rendering engine, it is the same sector and link machinery configured from a matrix, which means everything you learn about the Circos model applies to it.

The phylogenetic tree example takes a different route in. It starts from a Newick file, and there is a helper for the example trees:

python
from pycirclize import Circos
from pycirclize.utils import load_example_tree_file, ColorCycler
from matplotlib.lines import Line2D

# Initialize Circos from phylogenetic tree
tree_file = load_example_tree_file("large_example.nwk")
circos, tv = Circos.initialize_from_tree(
    tree_file,
    r_lim=(30, 100),
    leaf_label_size=5,
    line_kws=dict(color="lightgrey", lw=1.

The method is called initialize_from_tree and it returns two objects, the Circos instance and something called tv, which is the tree viewer. The r_lim parameter is the radius range the tree occupies, and leaf_label_size and line_kws control the labels and the branch lines. The fact that it returns a separate tree viewer object tells you the tree is not drawn as ordinary sectors but through its own path rendering, and the tv object is what you use to query or highlight parts of the tree.

That is the model for the whole library. The data is arranged in a circle, and the drawing primitives are radial bands, arcs, ribbons and tree paths. It is a focused tool for a specific visual grammar, and it does that grammar well.

The packaging, the toolchain and the optional interactive layer

The packaging metadata is small and modern, and the tooling choices are visible in the dependency groups.

The project requires Python 3.10 or later, and it is published both on PyPI and on conda-forge, with the install commands being:

bash
pip install pycirclize

and

bash
conda install -c conda-forge pycirclize

Having both is a deliberate choice for a scientific audience, since conda is how bioinformatics environments are usually built, where pinning the numpy and matplotlib versions against a complex scientific stack is often easier than fighting pip.

The runtime dependencies are biopython 1.80 or newer, matplotlib 3.6.3 or newer, numpy 1.21 or newer and pandas 1.3.5 or newer. Four dependencies, and the biopython one is the reason the genomic features work, since the Genbank parser in the example takes a Biopython feature object and reads feature.location.strand and feature.qualifiers.

There is one optional extra, tooltip, which pulls in ipympl 0.9.7 or newer. That is the interactive layer, and the README has a table of contents entry for the tooltip option, so it is a first class feature rather than an afterthought. Installing it means installing the Jupyter interactive matplotlib backend, which is a heavier dependency than the base package, which is why it is an extra.

The build backend is hatchling, with the version read from src/pycirclize/__init__.py, so the version lives in the package source rather than in the metadata. The pytest configuration runs with coverage over src and reports both XML and terminal, with long tracebacks and verbose output, and the tests live in tests/.

The linting is ruff with a line length of 88, which is the black default, and the selected rules are isort, pyflakes, pycodestyle errors and warnings, pydocstyle and pyupgrade. pydocstyle being enabled is a good signal, because it means every public function is expected to have a docstring, and it explains the density of the docstrings in the documentation. The lint group also includes pandas-stubs, which means the pandas typing is checked, and a tool called ty. The prek entry in the lint group is the pre-commit runner, and there is a prek.toml at the repository root, so the hooks are configured in the project rather than left to a global install.

The docs group is well built out, with mkdocs, the material theme, mkdocstrings for the API reference generated from the docstrings, mkdocs-jupyter for executable notebooks, markdown-callouts, and black. The jupyter group pulls in ipykernel, so the documentation examples can be run. And the genomics group pulls in pygenomeviz, which is a separate comparative genomics visualisation package, suggesting the author is working on adjacent tools in the same area.

The docs are published to a GitHub Pages site, and the mkdocs.yml at the repository root is the configuration. The CITATION.cff file at the root supplies citation metadata, which for a scientific visualisation package is the difference between a tool people use once and one that ends up in a methods section.

Editorial conclusion

pyCirclize fits a bioinformatician who needs a Circos style figure for a genome comparison, a comparative genomics view or a circular tree, and who wants the output to be a matplotlib figure they can compose with other plots. It is the wrong tool if you need a highly custom diagram outside the sector and track model, since the geometry is built around radial layout, and the wrong tool if you want interactive output rather than a static image. Before adopting, check the version you pin against the 1.10 line, because the API has moved across the 1.x series, and install the tooltip extra only if you need the interactive layer, since it pulls in ipympl.

Frequently asked questions

What is pyCirclize and what can it plot?

It is a Python package for circular visualisation implemented on matplotlib, for Circos plots, chord diagrams and radar charts, and it also implements genome and phylogenetic tree visualisation for bioinformatics. It was inspired by circlize and pyCircos.

How do I install pyCirclize?

Python 3.10 or later is required. Install the PyPI package with pip install pycirclize, or the conda-forge package with conda install -c conda-forge pycirclize, which is often easier in a bioinformatics environment where the numpy and matplotlib versions are already pinned.

How do I plot a genome with pyCirclize?

Fetch a GenBank record with fetch_genbank_by_accid, parse it with the Genbank class, get the sequence id to size mapping with get_seqid2size and pass it to Circos as the sectors. Then get the features with get_seqid2features for a feature type such as CDS, and plot them on two tracks per sector, one for each strand, using the genomic_features method with an arrow plot style.

Can I make a chord diagram with pyCirclize?

Yes, by passing a pandas DataFrame of the matrix to the Circos.chord_diagram class method, with keyword arguments for the sector labels and the link ribbons, for example label_kws and link_kws, plus a matplotlib colormap through cmap. The output is saved with circos.savefig.

Official sources

  1. License: MIT
  2. moshi4/pyCirclize on GitHub
  3. Project website
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/moshi4-pycirclize.svg)](https://hysenlabs.com/projects/moshi4-pycirclize)

Was this useful?