maphelios 0.8.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Zimmermann-Kogadeeva Group
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,77 @@
1
+ Metadata-Version: 2.4
2
+ Name: maphelios
3
+ Version: 0.8.3
4
+ Summary: Package for making genome related plots
5
+ Author-email: Bartosz Bartmanski <bartosz.bartmanski@embl.de>, Katarina Erbstein <katarina.erbstein@embl.de>
6
+ License: MIT License
7
+
8
+ Copyright (c) 2024 Zimmermann-Kogadeeva Group
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy
11
+ of this software and associated documentation files (the "Software"), to deal
12
+ in the Software without restriction, including without limitation the rights
13
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
+ copies of the Software, and to permit persons to whom the Software is
15
+ furnished to do so, subject to the following conditions:
16
+
17
+ The above copyright notice and this permission notice shall be included in all
18
+ copies or substantial portions of the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
+ SOFTWARE.
27
+
28
+ Project-URL: Homepage, https://git.embl.de/grp-zimmermann-kogadeeva/Maphelios
29
+ Project-URL: Bug tracker, https://git.embl.de/grp-zimmermann-kogadeeva/Maphelios/issues/
30
+ Requires-Python: >=3.9
31
+ Description-Content-Type: text/markdown
32
+ License-File: LICENSE
33
+ Requires-Dist: biopython
34
+ Requires-Dist: bcbio-gff
35
+ Requires-Dist: dna_features_viewer
36
+ Requires-Dist: matplotlib
37
+ Requires-Dist: pandas
38
+ Requires-Dist: platformdirs
39
+ Requires-Dist: pysam>=0.23.0
40
+ Requires-Dist: pycirclize>=1.9.1
41
+ Requires-Dist: scipy>=1.13.1
42
+ Provides-Extra: streamlit
43
+ Requires-Dist: streamlit; extra == "streamlit"
44
+ Requires-Dist: seaborn; extra == "streamlit"
45
+ Dynamic: license-file
46
+
47
+
48
+ # Maphelios
49
+
50
+ Package to find and plot inserts alongside the genome or contigs.
51
+
52
+ BLAST is required. You can follow the instructions to download and install
53
+ BLAST
54
+ [here](https://blast.ncbi.nlm.nih.gov/doc/blast-help/downloadblastdata.html).
55
+
56
+ ## Installation
57
+
58
+ ```
59
+ pip install git+https://git.embl.org/grp-zimmermann-kogadeeva/Maphelios.git
60
+ ```
61
+
62
+ ## Running
63
+
64
+ ### Docker
65
+
66
+ To run the streamlit app with docker:
67
+ ```
68
+ docker run -it --rm -p 8501:8501 docker://registry.git.embl.org/grp-zimmermann-kogadeeva/maphelios:0.7.3
69
+ ```
70
+
71
+ ### Apptainer
72
+
73
+ To run the streamlit app with apptainer:
74
+ ```
75
+ apptainer exec --no-home --writable-tmpfs --workdir /app docker://registry.git.embl.org/grp-zimmermann-kogadeeva/maphelios:0.7.3 streamlit run /app/app/app.py --server.headless true
76
+ ```
77
+
@@ -0,0 +1,31 @@
1
+
2
+ # Maphelios
3
+
4
+ Package to find and plot inserts alongside the genome or contigs.
5
+
6
+ BLAST is required. You can follow the instructions to download and install
7
+ BLAST
8
+ [here](https://blast.ncbi.nlm.nih.gov/doc/blast-help/downloadblastdata.html).
9
+
10
+ ## Installation
11
+
12
+ ```
13
+ pip install git+https://git.embl.org/grp-zimmermann-kogadeeva/Maphelios.git
14
+ ```
15
+
16
+ ## Running
17
+
18
+ ### Docker
19
+
20
+ To run the streamlit app with docker:
21
+ ```
22
+ docker run -it --rm -p 8501:8501 docker://registry.git.embl.org/grp-zimmermann-kogadeeva/maphelios:0.7.3
23
+ ```
24
+
25
+ ### Apptainer
26
+
27
+ To run the streamlit app with apptainer:
28
+ ```
29
+ apptainer exec --no-home --writable-tmpfs --workdir /app docker://registry.git.embl.org/grp-zimmermann-kogadeeva/maphelios:0.7.3 streamlit run /app/app/app.py --server.headless true
30
+ ```
31
+
@@ -0,0 +1,59 @@
1
+ [build-system]
2
+ requires = ["setuptools>=64.0.0", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "maphelios"
7
+ version = "0.8.3"
8
+ requires-python = ">=3.9"
9
+ description = "Package for making genome related plots"
10
+ readme = "README.md"
11
+ license = {file = "LICENSE"}
12
+ authors = [
13
+ { name = "Bartosz Bartmanski", email = "bartosz.bartmanski@embl.de" },
14
+ { name = "Katarina Erbstein", email = "katarina.erbstein@embl.de" }
15
+ ]
16
+
17
+ dependencies = [
18
+ "biopython",
19
+ "bcbio-gff",
20
+ "dna_features_viewer",
21
+ "matplotlib",
22
+ "pandas",
23
+ "platformdirs",
24
+ "pysam>=0.23.0",
25
+ "pycirclize>=1.9.1",
26
+ "scipy>=1.13.1",
27
+ ]
28
+
29
+ [project.optional-dependencies]
30
+ streamlit = ["streamlit", "seaborn"]
31
+
32
+ [project.scripts]
33
+ maphelios = "maphelios.__main__:main"
34
+
35
+ [project.urls]
36
+ "Homepage" = "https://git.embl.de/grp-zimmermann-kogadeeva/Maphelios"
37
+ "Bug tracker" = "https://git.embl.de/grp-zimmermann-kogadeeva/Maphelios/issues/"
38
+
39
+ [tool.isort]
40
+ profile = "black"
41
+ src_paths = ["src", "tests"]
42
+
43
+ [tool.bumpver]
44
+ current_version = "0.8.3"
45
+ version_pattern = "MAJOR.MINOR.PATCH"
46
+ commit_message = "Bump version {old_version} -> {new_version}"
47
+ commit = true
48
+ tag = true
49
+ push = true
50
+
51
+ [tool.bumpver.file_patterns]
52
+ "pyproject.toml" = ['current_version = "{version}"', 'version = "{version}"']
53
+ "src/maphelios/__init__.py" = ['^__version__ = "{version}"$']
54
+
55
+ [dependency-groups]
56
+ dev = [
57
+ "ipympl>=0.8.8",
58
+ "jupyterlab>=2.3.2",
59
+ ]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,5 @@
1
+ from .comparison import Comparison
2
+ from .insert import Insert
3
+ from .mapping import Mapping
4
+
5
+ __version__ = "0.8.3"
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env python3
2
+
3
+ from .cli import main
4
+
5
+ if __name__ == "__main__":
6
+ main()
@@ -0,0 +1,106 @@
1
+ import argparse
2
+
3
+ import matplotlib.pyplot as plt
4
+
5
+ from .main import Pipeline
6
+ from .plotting import plot_insert, plot_insert_dists, plot_genome
7
+
8
+
9
+ def get_args(*args):
10
+ # Create the CLI using argparse
11
+ # TODO: maybe switch from argparse to click
12
+ parser = argparse.ArgumentParser(description="Generating plots")
13
+
14
+ subparsers = parser.add_subparsers(help="sub-command help")
15
+
16
+ parser_ncbi = subparsers.add_parser("ncbi", help="Download genome from NCBI")
17
+ parser_ncbi.add_argument("search_term", help="search term for BLAST")
18
+ parser_ncbi.add_argument("email", help="Your email address - needed by BLAST")
19
+ parser_ncbi.add_argument(
20
+ "--retmax",
21
+ default=200,
22
+ help="Maximum number of records to download from NCBI",
23
+ )
24
+
25
+ parser_genome = subparsers.add_parser("file", help="Specify a local genome.")
26
+ parser_genome.add_argument("path_to_genome")
27
+
28
+ parser.add_argument("seq_file", help="file with the sequences")
29
+ parser.add_argument("output_prefix", help="Output directory")
30
+
31
+ parser.add_argument(
32
+ "--fwd_suffix", default="_F", help="Suffix for forward seq. default='_F'"
33
+ )
34
+ parser.add_argument(
35
+ "--rev_suffix", default="_R", help="Suffix for reverse seq. default='_R'"
36
+ )
37
+ parser.add_argument(
38
+ "--insert_types", help="matched, unmatched or both inserts", default="both"
39
+ )
40
+ parser.add_argument(
41
+ "--filter",
42
+ default=0.5,
43
+ help="Threshold value for filtering out blast results with low coverage",
44
+ )
45
+
46
+ # Get all the command line arguments
47
+ return parser.parse_args(*args)
48
+
49
+
50
+ def main():
51
+ args = get_args()
52
+ if "path_to_genome" in args:
53
+ search_term = None
54
+ email = None
55
+ retmax = None
56
+ path_to_genome = args.path_to_genome
57
+ else:
58
+ search_term = args.search_term
59
+ email = args.email
60
+ retmax = int(args.retmax)
61
+ path_to_genome = None
62
+
63
+ pipeline = Pipeline(
64
+ args.seq_file,
65
+ args.output_prefix,
66
+ genome_file=path_to_genome,
67
+ search_term=search_term,
68
+ email=email,
69
+ retmax=retmax,
70
+ fwd_suffix=args.fwd_suffix,
71
+ rev_suffix=args.rev_suffix,
72
+ )
73
+
74
+ # Create linear plots of inserts with annotations
75
+ for seq_id in pipeline.seq_ids:
76
+ inserts = pipeline.get_inserts(
77
+ seq_id, insert_types=args.insert_types, filter_threshold=args.filter
78
+ )
79
+ for i, insert in enumerate(inserts):
80
+ fig, axs = plt.subplots(2, 1, figsize=(10, 10), height_ratios=[3, 5])
81
+ axs = plot_insert(insert, pipeline.genome, axs=axs)
82
+ fig.savefig(pipeline.work_dir / f"{seq_id}_hit{i}.png", dpi=300)
83
+ plt.close()
84
+
85
+ # Get all inserts (used in both next plots)
86
+ inserts = pipeline.get_all_inserts(args.insert_types, filter_threshold=args.filter)
87
+
88
+ # Create a plot of genome / all contigs as circular plot with inserts
89
+ # layered on top
90
+ fig, ax = plt.subplots(figsize=(10, 30))
91
+ ax = plot_on_genome(inserts, pipeline.genome, ax=ax)
92
+ fig.savefig(pipeline.work_dir / "genome_plot.png", dpi=300)
93
+
94
+ # Plots of distributions of insert sizes
95
+ fig, axs = plt.subplots(1, 3, figsize=(12, 5))
96
+ axs = plot_insert_dists(inserts, axs=axs)
97
+ fig.savefig(pipeline.work_dir / "insert_length_dist.png", dpi=300)
98
+
99
+ df = pipeline.to_dataframe(
100
+ insert_types=args.insert_types, filter_threshold=args.filter
101
+ )
102
+ df.to_csv(pipeline.work_dir / "inserts.csv", index=False)
103
+
104
+
105
+ if __name__ == "__main__":
106
+ main()
@@ -0,0 +1,352 @@
1
+ from collections import defaultdict
2
+ from itertools import chain, cycle
3
+
4
+ import matplotlib.pyplot as plt
5
+ import numpy as np
6
+ import pandas as pd
7
+ from dna_features_viewer import CircularGraphicRecord
8
+ from matplotlib import color_sequences
9
+
10
+
11
+ class Clusters(dict):
12
+ def __init__(self, clusters, genomes=None):
13
+ super(Clusters, self).__init__(clusters)
14
+
15
+ self.genomes = genomes or {g for g, clust_idx in self.keys()}
16
+ self.cluster_labels = list(self.keys())
17
+
18
+ def __contains__(self, item):
19
+ return item in self.cluster_labels
20
+
21
+ @property
22
+ def insert_ids(self):
23
+ insert_ids = {
24
+ genome: list(
25
+ chain.from_iterable(
26
+ (
27
+ insert_ids
28
+ for (g, clust_idx), insert_ids in self.items()
29
+ if g == genome
30
+ )
31
+ )
32
+ )
33
+ for genome in self.genomes
34
+ }
35
+ return insert_ids
36
+
37
+ @property
38
+ def insert_labels(self):
39
+ seq_labels = defaultdict(dict)
40
+ for g, clust_idx in self.cluster_labels:
41
+ insert_ids = self.__getitem__((g, clust_idx))
42
+ # Set it such that only one label is given per cluster
43
+ for insert_id in insert_ids:
44
+ seq_labels[g][insert_id] = None
45
+ seq_labels[g][insert_ids[-1]] = f"Cluster {clust_idx}"
46
+ return seq_labels
47
+
48
+ @property
49
+ def other_repr(self):
50
+ other = defaultdict(list)
51
+ for (g, clust_idx), insert_ids in self.items():
52
+ other[g].append((clust_idx, insert_ids))
53
+ return other
54
+
55
+ def subset(self, selection):
56
+ return Clusters(
57
+ {
58
+ (g, clust_idx): self.__getitem__((g, clust_idx))
59
+ for g, clust_idx in selection
60
+ }
61
+ )
62
+
63
+
64
+ class Comparison(dict):
65
+
66
+ def __init__(self, *args, **kwargs):
67
+ super(Comparison, self).__init__(*args, **kwargs)
68
+
69
+ self.insert_ids = {g: mapping.insert_ids for g, mapping in self.items()}
70
+
71
+ def get_inserts(
72
+ self,
73
+ genomes=None,
74
+ seq_ids=None,
75
+ insert_ids=None,
76
+ insert_type="both",
77
+ filter_threshold=None,
78
+ **kwargs,
79
+ ):
80
+ if genomes is None:
81
+ genomes = self.keys()
82
+
83
+ return [
84
+ self.__getitem__(g).get(
85
+ insert_ids=insert_ids,
86
+ seq_ids=seq_ids,
87
+ insert_type=insert_type,
88
+ filter_threshold=filter_threshold,
89
+ )
90
+ for g in genomes
91
+ ]
92
+
93
+ def get_inserts_df(
94
+ self, genomes=None, insert_type="both", filter_threshold=None, **kwargs
95
+ ):
96
+ if genomes is None:
97
+ genomes = self.keys()
98
+
99
+ inserts_dfs = [
100
+ self.__getitem__(genome)
101
+ .to_dataframe(insert_type=insert_type, filter_threshold=filter_threshold)
102
+ .assign(genome=genome)
103
+ for genome in genomes
104
+ ]
105
+
106
+ if len(inserts_dfs):
107
+ return pd.concat(inserts_dfs, ignore_index=True)
108
+
109
+ def get_insert_presence_df(
110
+ self, genomes=None, insert_type="both", filter_threshold=None, **kwargs
111
+ ):
112
+ if genomes is None:
113
+ genomes = self.keys()
114
+
115
+ dfs = [
116
+ pd.DataFrame(
117
+ self.__getitem__(g).get_seq_ids(insert_type, filter_threshold),
118
+ columns=["insert_ids"],
119
+ ).assign(genome=g)
120
+ for g in genomes
121
+ ]
122
+
123
+ all_seq_ids = sorted(
124
+ set(chain.from_iterable(self.__getitem__(g).seq_ids for g in genomes))
125
+ )
126
+
127
+ df_insert_presence = (
128
+ pd.concat(dfs, ignore_index=True)
129
+ .groupby(["insert_ids", "genome"], as_index=False)
130
+ .agg(num_inserts=pd.NamedAgg("insert_ids", "count"))
131
+ .pivot(index="insert_ids", columns="genome", values="num_inserts")
132
+ .reindex(columns=genomes, index=all_seq_ids)
133
+ )
134
+
135
+ return df_insert_presence
136
+
137
+ def get_clusters(
138
+ self,
139
+ genomes=None,
140
+ insert_type="both",
141
+ filter_threshold=None,
142
+ plain_dict=True,
143
+ **kwargs,
144
+ ):
145
+ if genomes is None:
146
+ genomes = self.keys()
147
+
148
+ if plain_dict:
149
+ return {
150
+ g: self.__getitem__(g).get_clusters(insert_type, filter_threshold)
151
+ for g in genomes
152
+ }
153
+ else:
154
+ return Clusters(
155
+ {
156
+ (g, clust_idx): cluster
157
+ for g in genomes
158
+ for clust_idx, cluster in enumerate(
159
+ self.__getitem__(g).get_clusters(insert_type, filter_threshold)
160
+ )
161
+ },
162
+ genomes=genomes,
163
+ )
164
+
165
+ def get_labels(
166
+ self,
167
+ seq_ids,
168
+ insert_ids=None,
169
+ genomes=None,
170
+ insert_type="both",
171
+ filter_threshold=None,
172
+ ):
173
+ if genomes is None:
174
+ genomes = self.keys()
175
+
176
+ if insert_ids is None:
177
+ insert_ids = {g: None for g in genomes}
178
+
179
+ if seq_ids is not None or insert_ids is not None:
180
+ return {
181
+ g: self.__getitem__(g).get_labels(
182
+ seq_ids, insert_ids.get(g), genomes, insert_type, filter_threshold
183
+ )
184
+ for g in genomes
185
+ }
186
+
187
+ def get_genes_df(
188
+ self,
189
+ seq_ids=None,
190
+ insert_ids=None,
191
+ insert_type="both",
192
+ filter_threshold=None,
193
+ buffer=4000,
194
+ **kwargs,
195
+ ):
196
+ if seq_ids is None:
197
+ seq_ids = {x: None for x in self.keys()}
198
+ elif isinstance(seq_ids, (list, tuple)):
199
+ seq_ids = {x: seq_ids for x in self.keys()}
200
+
201
+ if insert_ids is None:
202
+ insert_ids = {x: None for x in self.keys()}
203
+ elif isinstance(insert_ids, (list, tuple)):
204
+ insert_ids = {x: insert_ids for x in self.keys()}
205
+
206
+ genomes = set(seq_ids.keys()) | set(insert_ids.keys())
207
+
208
+ genes_dfs = [
209
+ self.__getitem__(g)
210
+ .genes_to_dataframe(
211
+ seq_ids=seq_ids.get(g),
212
+ insert_ids=insert_ids.get(g),
213
+ insert_type=insert_type,
214
+ filter_threshold=filter_threshold,
215
+ buffer=buffer,
216
+ )
217
+ .assign(genome=g)
218
+ for g in genomes
219
+ ]
220
+
221
+ if len(genes_dfs):
222
+ drop_by = ["start", "end", "type", "insert_idx", "seq_id", "genome"]
223
+ df_genes = pd.concat(genes_dfs, ignore_index=True).drop_duplicates(
224
+ subset=drop_by
225
+ )
226
+ df_genes.insert(0, "genome", df_genes.pop("genome"))
227
+ return df_genes
228
+
229
+ def get_graphic_features(
230
+ self,
231
+ seq_ids=None,
232
+ insert_ids=None,
233
+ genomes_order=None,
234
+ seq_labels=None,
235
+ show_contig_labels=True,
236
+ insert_type="both",
237
+ filter_threshold=None,
238
+ palette="tab10",
239
+ **kwargs,
240
+ ):
241
+ if seq_ids is None:
242
+ seq_ids = {x: None for x in self.keys()}
243
+ elif isinstance(seq_ids, (list, tuple)):
244
+ seq_ids = {x: seq_ids for x in self.keys()}
245
+
246
+ if insert_ids is None:
247
+ insert_ids = {x: None for x in self.keys()}
248
+ elif isinstance(insert_ids, (list, tuple)):
249
+ insert_ids = {x: insert_ids for x in self.keys()}
250
+
251
+ if genomes_order is None:
252
+ genomes_order = set(seq_ids.keys()) | set(insert_ids.keys())
253
+
254
+ if seq_labels is None:
255
+ seq_labels = {x: None for x in self.keys()}
256
+
257
+ palette = cycle(color_sequences[palette])
258
+
259
+ res = {
260
+ g: self.__getitem__(g).get_graphic_features(
261
+ seq_ids=seq_ids.get(g),
262
+ insert_ids=insert_ids.get(g),
263
+ insert_type=insert_type,
264
+ filter_threshold=filter_threshold,
265
+ contig_labels=show_contig_labels,
266
+ seq_labels=seq_labels.get(g),
267
+ col1=col,
268
+ col2=col,
269
+ linecolor=col,
270
+ **kwargs,
271
+ )
272
+ for g, col in zip(genomes_order, palette)
273
+ }
274
+
275
+ return res
276
+
277
+ def plot(
278
+ self,
279
+ seq_ids=None,
280
+ insert_ids=None,
281
+ genomes_order=None,
282
+ seq_labels=None,
283
+ insert_type="both",
284
+ filter_threshold=None,
285
+ feature_kwargs=None,
286
+ show_contig_labels=True,
287
+ show_titles=False,
288
+ palette="tab10",
289
+ facet_wrap=None,
290
+ fig=None,
291
+ **kwargs,
292
+ ):
293
+ feature_kwargs = {} if feature_kwargs is None else feature_kwargs
294
+
295
+ # Get the graphic feature objects
296
+ res = self.get_graphic_features(
297
+ seq_ids,
298
+ insert_ids,
299
+ genomes_order,
300
+ seq_labels,
301
+ show_contig_labels,
302
+ insert_type,
303
+ filter_threshold,
304
+ palette,
305
+ **feature_kwargs,
306
+ )
307
+
308
+ # Set all the plotting options based on input
309
+ if "figsize" not in kwargs:
310
+ kwargs["figsize"] = (10, 8)
311
+
312
+ genome_labels = res.keys()
313
+ num_genomes = len(genome_labels)
314
+
315
+ # Set the number of axes in the figure based on facet_wrap option
316
+ nrows, ncols = 1, 1
317
+ if facet_wrap is not None:
318
+ ncols = min(facet_wrap, num_genomes)
319
+ nrows = num_genomes // ncols
320
+
321
+ # Create figure and axes if not provided
322
+ if fig is None:
323
+ fig, axs = plt.subplots(nrows=nrows, ncols=ncols, **kwargs)
324
+ else:
325
+ axs = fig.get_axes()
326
+
327
+ # Check that axs is iterable even if a single axes object
328
+ if isinstance(axs, plt.Axes):
329
+ axs = np.array([axs])
330
+
331
+ # Make and label all the plots
332
+ if facet_wrap is None:
333
+ features = list(chain.from_iterable([x[1] for x in res.values()]))
334
+ rec = CircularGraphicRecord(max([x[0] for x in res.values()]), features)
335
+ _ = rec.plot(axs[0], annotate_inline=False)
336
+
337
+ if show_titles:
338
+ axs[0].set_title(", ".join(list(genome_labels)))
339
+
340
+ else:
341
+ for (label, (seq_len, features)), ax in zip(res.items(), axs.flatten()):
342
+ rec = CircularGraphicRecord(seq_len, features)
343
+ _ = rec.plot(ax, annotate_inline=False)
344
+
345
+ if show_titles:
346
+ ax.set_title(label)
347
+
348
+ # Remove redundant axis
349
+ for ax in axs[num_genomes:]:
350
+ ax.axis("off")
351
+
352
+ return fig