trackplot 0.7.0__tar.gz → 0.7.1__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.
Files changed (53) hide show
  1. {trackplot-0.7.0/trackplot.egg-info → trackplot-0.7.1}/PKG-INFO +1 -1
  2. {trackplot-0.7.0 → trackplot-0.7.1}/pyproject.toml +1 -1
  3. {trackplot-0.7.0 → trackplot-0.7.1}/trackplot/base/Readder.py +15 -5
  4. {trackplot-0.7.0 → trackplot-0.7.1}/trackplot/base/Transcript.py +1 -1
  5. {trackplot-0.7.0 → trackplot-0.7.1}/trackplot/file/Annotation.py +138 -74
  6. {trackplot-0.7.0 → trackplot-0.7.1}/trackplot/file/Bam.py +1 -0
  7. {trackplot-0.7.0 → trackplot-0.7.1}/trackplot/plot/render.py +6 -2
  8. {trackplot-0.7.0 → trackplot-0.7.1/trackplot.egg-info}/PKG-INFO +1 -1
  9. {trackplot-0.7.0 → trackplot-0.7.1}/LICENSE +0 -0
  10. {trackplot-0.7.0 → trackplot-0.7.1}/README.md +0 -0
  11. {trackplot-0.7.0 → trackplot-0.7.1}/setup.cfg +0 -0
  12. {trackplot-0.7.0 → trackplot-0.7.1}/trackplot/__init__.py +0 -0
  13. {trackplot-0.7.0 → trackplot-0.7.1}/trackplot/anno/AxLabel.py +0 -0
  14. {trackplot-0.7.0 → trackplot-0.7.1}/trackplot/anno/__init__.py +0 -0
  15. {trackplot-0.7.0 → trackplot-0.7.1}/trackplot/anno/theme.py +0 -0
  16. {trackplot-0.7.0 → trackplot-0.7.1}/trackplot/base/CoordinateMap.py +0 -0
  17. {trackplot-0.7.0 → trackplot-0.7.1}/trackplot/base/GenomicLoci.py +0 -0
  18. {trackplot-0.7.0 → trackplot-0.7.1}/trackplot/base/Junction.py +0 -0
  19. {trackplot-0.7.0 → trackplot-0.7.1}/trackplot/base/Protein.py +0 -0
  20. {trackplot-0.7.0 → trackplot-0.7.1}/trackplot/base/ReadDepth.py +0 -0
  21. {trackplot-0.7.0 → trackplot-0.7.1}/trackplot/base/Stroke.py +0 -0
  22. {trackplot-0.7.0 → trackplot-0.7.1}/trackplot/base/__init__.py +0 -0
  23. {trackplot-0.7.0 → trackplot-0.7.1}/trackplot/base/pyUniprot.py +0 -0
  24. {trackplot-0.7.0 → trackplot-0.7.1}/trackplot/cli.py +0 -0
  25. {trackplot-0.7.0 → trackplot-0.7.1}/trackplot/conf/DomainSetting.py +0 -0
  26. {trackplot-0.7.0 → trackplot-0.7.1}/trackplot/conf/__init__.py +0 -0
  27. {trackplot-0.7.0 → trackplot-0.7.1}/trackplot/conf/config.py +0 -0
  28. {trackplot-0.7.0 → trackplot-0.7.1}/trackplot/conf/drawing.py +0 -0
  29. {trackplot-0.7.0 → trackplot-0.7.1}/trackplot/conf/ui.py +0 -0
  30. {trackplot-0.7.0 → trackplot-0.7.1}/trackplot/file/ATAC.py +0 -0
  31. {trackplot-0.7.0 → trackplot-0.7.1}/trackplot/file/BedGraph.py +0 -0
  32. {trackplot-0.7.0 → trackplot-0.7.1}/trackplot/file/Bigwig.py +0 -0
  33. {trackplot-0.7.0 → trackplot-0.7.1}/trackplot/file/Depth.py +0 -0
  34. {trackplot-0.7.0 → trackplot-0.7.1}/trackplot/file/Fasta.py +0 -0
  35. {trackplot-0.7.0 → trackplot-0.7.1}/trackplot/file/File.py +0 -0
  36. {trackplot-0.7.0 → trackplot-0.7.1}/trackplot/file/HiCMatrixTrack.py +0 -0
  37. {trackplot-0.7.0 → trackplot-0.7.1}/trackplot/file/Junction.py +0 -0
  38. {trackplot-0.7.0 → trackplot-0.7.1}/trackplot/file/Motif.py +0 -0
  39. {trackplot-0.7.0 → trackplot-0.7.1}/trackplot/file/ReadSegments.py +0 -0
  40. {trackplot-0.7.0 → trackplot-0.7.1}/trackplot/file/__init__.py +0 -0
  41. {trackplot-0.7.0 → trackplot-0.7.1}/trackplot/plot/__init__.py +0 -0
  42. {trackplot-0.7.0 → trackplot-0.7.1}/trackplot/plot/coord.py +0 -0
  43. {trackplot-0.7.0 → trackplot-0.7.1}/trackplot/plot/core.py +0 -0
  44. {trackplot-0.7.0 → trackplot-0.7.1}/trackplot/plot/info.py +0 -0
  45. {trackplot-0.7.0 → trackplot-0.7.1}/trackplot/plot/limits.py +0 -0
  46. {trackplot-0.7.0 → trackplot-0.7.1}/trackplot/plot/utils.py +0 -0
  47. {trackplot-0.7.0 → trackplot-0.7.1}/trackplot/plot_func.py +0 -0
  48. {trackplot-0.7.0 → trackplot-0.7.1}/trackplot/server.py +0 -0
  49. {trackplot-0.7.0 → trackplot-0.7.1}/trackplot.egg-info/SOURCES.txt +0 -0
  50. {trackplot-0.7.0 → trackplot-0.7.1}/trackplot.egg-info/dependency_links.txt +0 -0
  51. {trackplot-0.7.0 → trackplot-0.7.1}/trackplot.egg-info/entry_points.txt +0 -0
  52. {trackplot-0.7.0 → trackplot-0.7.1}/trackplot.egg-info/requires.txt +0 -0
  53. {trackplot-0.7.0 → trackplot-0.7.1}/trackplot.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: trackplot
3
- Version: 0.7.0
3
+ Version: 0.7.1
4
4
  Summary: The trackplot is a tool for visualizing various next-generation sequencing (NGS) data, including DNA-seq, RNA-seq, single-cell RNA-seq and full-length sequencing datasets. https://sashimi.readthedocs.io/
5
5
  Author-email: ygidtu <ygidtu@gmail.com>
6
6
  License-Expression: BSD-3-Clause
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "trackplot"
3
- version = "0.7.0"
3
+ version = "0.7.1"
4
4
  description = "The trackplot is a tool for visualizing various next-generation sequencing (NGS) data, including DNA-seq, RNA-seq, single-cell RNA-seq and full-length sequencing datasets. https://sashimi.readthedocs.io/"
5
5
  authors = [
6
6
  { name = "ygidtu", email = "ygidtu@gmail.com" }
@@ -136,20 +136,30 @@ class Reader(object):
136
136
  yield read, __get_strand__(read, library=library)
137
137
 
138
138
  @classmethod
139
- def read_gtf(cls, path: str, region: GenomicLoci, bed: bool = False):
139
+ def read_gtf(
140
+ cls,
141
+ path: str,
142
+ region: GenomicLoci,
143
+ bed: bool = False,
144
+ fmt: Optional[str] = None,
145
+ ):
146
+ if bed:
147
+ parser = pysam.asBed()
148
+ elif fmt == "gff":
149
+ parser = pysam.asGFF3()
150
+ else:
151
+ parser = pysam.asGTF()
140
152
  with pysam.TabixFile(path) as r:
141
153
  try:
142
154
  iter_ = r.fetch(
143
155
  region.chromosome,
144
156
  region.start,
145
157
  region.end,
146
- parser=pysam.asGTF() if not bed else pysam.asBed(),
158
+ parser=parser,
147
159
  )
148
160
  except ValueError:
149
161
  try:
150
- iter_ = cls.__modify_chrom__(
151
- region, r, parser=pysam.asGTF() if not bed else pysam.asBed()
152
- )
162
+ iter_ = cls.__modify_chrom__(region, r, parser=parser)
153
163
  except ValueError as err:
154
164
  logger.debug("please check the input region and gtf files")
155
165
  logger.error(err)
@@ -7,7 +7,7 @@ Changelog:
7
7
  1. remove attributes
8
8
  """
9
9
 
10
- from typing import List
10
+ from typing import List, Optional
11
11
 
12
12
  from trackplot.base.GenomicLoci import GenomicLoci
13
13
 
@@ -33,6 +33,7 @@ class Annotation(File):
33
33
 
34
34
  __slots__ = (
35
35
  "category",
36
+ "annotation_format",
36
37
  "add_domain",
37
38
  "domain",
38
39
  "interval_file",
@@ -64,7 +65,11 @@ class Annotation(File):
64
65
  f"category should be one of {categories}, instead of {category}"
65
66
  )
66
67
 
67
- super().__init__(path=self.index_gtf(path) if category == "gtf" else path)
68
+ self.annotation_format = None
69
+ if category == "gtf":
70
+ self.annotation_format = self._detect_format(path)
71
+ path = self.index_gtf(path)
72
+ super().__init__(path=path)
68
73
  self.category = category
69
74
  self.data = []
70
75
  self.add_domain = add_domain
@@ -270,42 +275,35 @@ class Annotation(File):
270
275
  return self
271
276
 
272
277
  @staticmethod
273
- def is_gtf(infile):
274
- """
275
- check if input file is gtf
276
- :param infile: path to input file
277
- :return:
278
+ def _detect_format(infile):
279
+ """Detect annotation file format: 'gtf', 'gff' (GFF3) or None.
280
+
281
+ GTF uses double-quoted attribute values (``key "value";``) while GFF3
282
+ uses ``key=value``. Only the first non-comment data line is inspected.
278
283
  """
279
284
  if infile is None:
280
- return False
281
-
282
- is_gtf = 0
285
+ return None
283
286
  try:
284
287
  if filetype.guess_mime(infile) == "application/gzip":
285
- is_gtf += 10
286
- r = gzip.open(infile, "rt")
288
+ handle = gzip.open(infile, "rt")
287
289
  else:
288
- r = open(infile)
289
-
290
- for line in r:
291
- if line.startswith("#"):
292
- continue
293
-
294
- lines = re.split(r"\s+", line)
295
-
296
- if len(lines) < 8:
297
- break
298
-
299
- if re.search(r"([\w-]+ \"[\w.\s\-%,:]+\";? ?)+", " ".join(lines[8:])):
300
- is_gtf += 1
301
-
302
- break
303
-
304
- r.close()
305
- return is_gtf
306
- except TypeError as err:
307
- logger.error("failed to open %s", infile)
308
- exit(err)
290
+ handle = open(infile)
291
+ with handle:
292
+ for line in handle:
293
+ if line.startswith("#"):
294
+ continue
295
+ columns = line.rstrip("\n").split("\t")
296
+ if len(columns) < 9:
297
+ return None
298
+ attributes = columns[8]
299
+ if '"' in attributes:
300
+ return "gtf"
301
+ if "=" in attributes:
302
+ return "gff"
303
+ return None
304
+ except (OSError, UnicodeDecodeError):
305
+ return None
306
+ return None
309
307
 
310
308
  @classmethod
311
309
  def sort_gtf(cls, input_gtf: str, output_gtf: str):
@@ -329,9 +327,9 @@ class Annotation(File):
329
327
  w.write(line)
330
328
  continue
331
329
 
332
- lines = line.split()
330
+ lines = line.rstrip("\n").split("\t")
333
331
 
334
- if len(lines) < 1:
332
+ if len(lines) < 7:
335
333
  continue
336
334
 
337
335
  data.append(
@@ -364,10 +362,10 @@ class Annotation(File):
364
362
  :param input_gtf: path to input gtf file
365
363
  :return path to compressed and indexed bgzipped gtf file
366
364
  """
367
- gtf = cls.is_gtf(input_gtf)
368
- assert gtf % 10 == 1, f"{input_gtf} seems not be gtf format"
365
+ fmt = cls._detect_format(input_gtf)
366
+ assert fmt in ("gtf", "gff"), f"{input_gtf} seems not be gtf/gff format"
369
367
 
370
- if gtf // 10 > 0:
368
+ if filetype.guess_mime(input_gtf) == "application/gzip":
371
369
  output_gtf = input_gtf
372
370
  else:
373
371
  output_gtf = input_gtf + ".gz"
@@ -381,7 +379,10 @@ class Annotation(File):
381
379
  except OSError as err:
382
380
  logger.debug(f"Guess gtf needs to be sorted: {err}")
383
381
 
384
- sorted_gtf = re.sub(r"\.gtf(.gz)?$", "", input_gtf) + ".sorted.gtf.gz"
382
+ sorted_gtf = (
383
+ re.sub(r"\.(gtf|gff3?)(\.gz)?$", "", input_gtf)
384
+ + f".sorted.{fmt}.gz"
385
+ )
385
386
  if os.path.exists(sorted_gtf) and os.path.exists(sorted_gtf + ".tbi"):
386
387
  return sorted_gtf
387
388
 
@@ -390,11 +391,76 @@ class Annotation(File):
390
391
  sorted_gtf, preset="gff", force=True, keep_original=True
391
392
  )
392
393
  return sorted_gtf
393
- elif os.path.getctime(output_gtf) < os.path.getctime(output_gtf):
394
+ elif os.path.getctime(output_gtf + ".tbi") < os.path.getctime(output_gtf):
394
395
  logger.info("the tbi index is older than the gtf file")
395
396
 
396
397
  return output_gtf
397
398
 
399
+ @staticmethod
400
+ def _rec_attr(rec, *names, default=None):
401
+ """Fetch an attribute from a pysam GTF or GFF3 record, defensively.
402
+
403
+ ``asGTF`` exposes convenience properties (e.g. ``rec.gene_id``) and
404
+ ``rec.attributes`` as a string, whereas ``asGFF3`` only exposes
405
+ ``rec.asDict()`` and raises ``KeyError`` on convenience properties such
406
+ as ``rec.gene_id``. This helper tries, in order: convenience properties,
407
+ subscript access ``rec[name]``, then the parsed attributes dict,
408
+ returning the first truthy match.
409
+ """
410
+ for name in names:
411
+ try:
412
+ value = getattr(rec, name)
413
+ except Exception:
414
+ value = None
415
+ if value:
416
+ return value
417
+ for name in names:
418
+ try:
419
+ value = rec[name]
420
+ except Exception:
421
+ value = None
422
+ if value:
423
+ return value
424
+ try:
425
+ attrs = rec.asDict() if hasattr(rec, "asDict") else rec.attributes
426
+ except Exception:
427
+ attrs = None
428
+ if isinstance(attrs, dict):
429
+ for name in names:
430
+ value = attrs.get(name)
431
+ if value:
432
+ return value
433
+ return default
434
+
435
+ @staticmethod
436
+ def _normalize_transcript_id(value):
437
+ """Strip GFF3 relationship prefixes (e.g. ``rna-``) from a transcript id.
438
+
439
+ GFF3 links a feature to its transcript through ``Parent=rna-XM_...``
440
+ while the transcript record exposes ``transcript_id=XM_...`` (and
441
+ ``ID=rna-XM_...``). Normalizing both sides to the bare identifier keeps
442
+ CDS/exon records attached to their transcript, which is required for
443
+ faithful UTR/CDS coloring.
444
+ """
445
+ if not value:
446
+ return value
447
+ for prefix in ("rna-", "rna:", "transcript-", "transcript:", "mrna-"):
448
+ if value.startswith(prefix):
449
+ return value[len(prefix):]
450
+ return value
451
+
452
+ def _transcript_key(self, rec):
453
+ """Resolve a stable transcript identifier for GTF and GFF3 records.
454
+
455
+ Child features (exon/CDS) reference the transcript via ``Parent`` in
456
+ GFF3, whereas the transcript record itself references it via ``ID``.
457
+ """
458
+ if re.search(r"(cds|exon)", rec.feature, re.I):
459
+ value = self._rec_attr(rec, "transcript_id", "Parent", default=None)
460
+ else:
461
+ value = self._rec_attr(rec, "transcript_id", "ID", default=None)
462
+ return self._normalize_transcript_id(value)
463
+
398
464
  def __load_gtf__(self, transcripts_to_show: list[str] | None = None):
399
465
  """
400
466
  Load transcripts inside of region from gtf file
@@ -405,7 +471,11 @@ class Annotation(File):
405
471
  transcripts = {}
406
472
  exons = {}
407
473
  coding = {}
408
- for rec in Reader.read_gtf(self.path, self.region):
474
+ for rec in Reader.read_gtf(
475
+ self.path,
476
+ self.region,
477
+ fmt=getattr(self, "annotation_format", None),
478
+ ):
409
479
  start = max(rec.start, self.region.start)
410
480
  end = min(rec.end, self.region.end)
411
481
 
@@ -414,59 +484,53 @@ class Annotation(File):
414
484
  if start + 1 >= self.region.end:
415
485
  break
416
486
 
487
+ rec_id = self._transcript_key(rec)
488
+
417
489
  if re.search(r"(rna|transcript|cds)", rec.feature, re.I):
418
490
  if transcripts_to_show:
419
- _name = (
420
- rec.transcript_name
421
- if "transcript_name" in rec.attributes
422
- else rec.transcript_id
423
- )
491
+ _name = self._rec_attr(rec, "transcript_name", "Name") or rec_id
424
492
  if _name not in transcripts_to_show:
425
493
  logger.info(f"Skipping transcript {_name}")
426
494
  continue
427
495
 
428
- if rec.transcript_id not in transcripts.keys():
429
- transcripts[rec.transcript_id] = Transcript(
496
+ if rec_id and rec_id not in transcripts.keys():
497
+ transcripts[rec_id] = Transcript(
430
498
  chromosome=rec.contig,
431
499
  start=start,
432
500
  end=end,
433
501
  strand=rec.strand,
434
- transcript_id=rec.transcript_id,
435
- gene_id=rec.gene_id,
436
- gene=rec.gene_name if "gene_name" in rec.attributes else "",
437
- transcript=rec.transcript_name
438
- if "transcript_name" in rec.attributes
439
- else "",
502
+ transcript_id=rec_id,
503
+ gene_id=self._rec_attr(
504
+ rec, "gene_id", "gene", "ID", default=""
505
+ ),
506
+ gene=self._rec_attr(rec, "gene_name", default=""),
507
+ transcript=self._rec_attr(
508
+ rec, "transcript_name", "Name", default=""
509
+ ),
440
510
  exons=[],
441
511
  )
442
512
 
443
513
  if re.search(r"(cds)", rec.feature, re.I):
444
- coding.setdefault(rec.transcript_id, []).append((start, end))
514
+ if rec_id:
515
+ coding.setdefault(rec_id, []).append((start, end))
445
516
 
446
517
  elif re.search(r"(exon)", rec.feature, re.I):
447
- if rec.transcript_id not in exons.keys():
448
- exons[rec.transcript_id] = []
518
+ if rec_id and rec_id not in exons.keys():
519
+ exons[rec_id] = []
449
520
 
450
521
  # fix exon_id error
451
- exon_id = ""
452
- if "exon_id" in rec.keys():
453
- exon_id = rec.exon_id
454
- elif "ID" in rec.keys():
455
- exon_id = rec["ID"]
456
- elif "id" in rec.keys():
457
- exon_id = rec["id"]
458
- elif "_id" in rec.keys():
459
- exon_id = rec["_id"]
460
-
461
- exons[rec.transcript_id].append(
462
- GenomicLoci(
463
- chromosome=rec.contig,
464
- start=start,
465
- end=end,
466
- strand=rec.strand,
467
- name=exon_id,
522
+ exon_id = self._rec_attr(rec, "exon_id", "ID", "id", "_id", default="")
523
+
524
+ if rec_id:
525
+ exons[rec_id].append(
526
+ GenomicLoci(
527
+ chromosome=rec.contig,
528
+ start=start,
529
+ end=end,
530
+ strand=rec.strand,
531
+ name=exon_id,
532
+ )
468
533
  )
469
- )
470
534
 
471
535
  for key, trans in transcripts.items():
472
536
  if key in exons.keys():
@@ -193,6 +193,7 @@ class Bam(SingleCell):
193
193
  :param normalize_format: None -> raw counts; others fpkm and cpm
194
194
  """
195
195
  self.region = region
196
+ self.normalize_format = normalize_format
196
197
 
197
198
  spanned_junctions = kwargs.get("junctions", {})
198
199
  included_junctions = kwargs.get("included_junctions", {})
@@ -213,7 +213,7 @@ def plot_annotation(
213
213
  ]
214
214
  segments = (
215
215
  _split_exon_into_cds_utr(s, e, exon_coding)
216
- if show_utr and exon_coding
216
+ if show_utr
217
217
  else [(s, e, True)]
218
218
  )
219
219
  for seg_s, seg_e, is_cds in segments:
@@ -593,6 +593,10 @@ def plot_density(
593
593
  for k, v in jxns.items():
594
594
  jxns[k] = np.log1p(v) / denominator
595
595
 
596
+ normalize_format = getattr(obj, "normalize_format", None)
597
+ if normalize_format and normalize_format != "count":
598
+ y_label += f" ({normalize_format})"
599
+
596
600
  # Compute data-driven baseline arc heights BEFORE calling _compute_y_limits.
597
601
  # These must match the internal computation in _compute_y_limits so that the
598
602
  # drawn junction arcs have exactly the same height that was used to expand
@@ -1207,7 +1211,7 @@ def plot_igv_like(
1207
1211
  ]
1208
1212
  segments = (
1209
1213
  _split_exon_into_cds_utr(s, e, exon_coding)
1210
- if show_utr and exon_coding
1214
+ if show_utr
1211
1215
  else [(s, e, True)]
1212
1216
  )
1213
1217
  for seg_s, seg_e, is_cds in segments:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: trackplot
3
- Version: 0.7.0
3
+ Version: 0.7.1
4
4
  Summary: The trackplot is a tool for visualizing various next-generation sequencing (NGS) data, including DNA-seq, RNA-seq, single-cell RNA-seq and full-length sequencing datasets. https://sashimi.readthedocs.io/
5
5
  Author-email: ygidtu <ygidtu@gmail.com>
6
6
  License-Expression: BSD-3-Clause
File without changes
File without changes
File without changes
File without changes
File without changes