Repository: cogent3/cogent3
Pull request #1518 (2023-07-10 05:30:19)

MAINT tweaks to drawing features


Patch:
diff --git a/src/cogent3/core/alignment.py b/src/cogent3/core/alignment.py
index 4202067f8..e0d64d090 100644
--- a/src/cogent3/core/alignment.py
+++ b/src/cogent3/core/alignment.py
@@ -2479,9 +2479,6 @@ class Aligned:
         for annot in self.data.get_features(biotype=annotation_type, **kwargs):
             yield annot.remapped_to(alignment, self.map.inverse())
 
-    def get_drawables(self):
-        return self.data.get_drawables()
-
     def gap_vector(self):
         """Returns gap_vector of GappedSeq, for omit_gap_pos."""
         return self.get_gapped_seq().gap_vector()
@@ -3787,45 +3784,6 @@ class AlignmentI(object):
             width=width, height=height, wrap=wrap, vspace=vspace, colours=colours
         )
 
-    def get_drawable(self, width=600, vertical=False):
-        """returns Drawable instance"""
-        from cogent3.draw.drawable import Drawable
-
-        drawables = self.get_drawables()
-        if not drawables:
-            return None
-        # we order by tracks
-        top = 0
-        space = 0.25
-        annotes = []
-        for feature_type in drawables:
-            new_bottom = top + space
-            for i, annott in enumerate(drawables[feature_type]):
-                annott.shift(y=new_bottom - annott.bottom)
-                if i > 0:
-                    annott._showlegend = False
-                annotes.append(annott)
-
-            top = annott.top
-
-        top += space
-        height = max((top / len(self)) * width, 300)
-        xaxis = dict(range=[0, len(self)], zeroline=False, showline=True)
-        yaxis = dict(range=[0, top], visible=False, zeroline=True, showline=True)
-
-        if vertical:
-            all_traces = [t.T.as_trace() for t in annotes]
-            width, height = height, width
-            xaxis, yaxis = yaxis, xaxis
-        else:
-            all_traces = [t.as_trace() for t in annotes]
-
-        drawer = Drawable(
-            title=self.name, traces=all_traces, width=width, height=height
-        )
-        drawer.layout.update(xaxis=xaxis, yaxis=yaxis)
-        return drawer
-
 
 def _one_length(seqs):
     """raises ValueError if seqs not all same length"""
@@ -5185,13 +5143,85 @@ class Alignment(AlignmentI, SequenceCollection):
 
         return self.__class__(new_seqs, info=self.info)
 
-    def get_drawables(self):
-        """returns a dict of drawables, keyed by type"""
+    def get_drawables(self, *, biotype: Optional[str, Iterable[str]] = None) -> dict:
+        """returns a dict of drawables, keyed by type
+
+        Parameters
+        ----------
+        biotype
+            passed to get_features(biotype). Can be a single biotype or
+            series. Only features matching this will be included.
+        """
         result = defaultdict(list)
-        for f in self.get_features():
+        for f in self.get_features(biotype=biotype, allow_partial=True):
             result[f.biotype].append(f.get_drawable())
         return result
 
+    def get_drawable(
+        self,
+        *,
+        biotype: Optional[str, Iterable[str]] = None,
+        width: int = 600,
+        vertical: int = False,
+    ):
+        """make a figure from sequence features
+
+        Parameters
+        ----------
+        biotype
+            passed to get_features(biotype). Can be a single biotype or
+            series. Only features matching this will be included.
+        width
+            width in pixels
+        vertical
+            rotates the drawable
+
+        Returns
+        -------
+        a Drawable instance
+        """
+        # todo gah I think this needs to be modified to make row-blocks
+        # for each sequence in the alignment, or are we displaying the
+        # sequence name in the feature label?
+        from cogent3.draw.drawable import Drawable
+
+        drawables = self.get_drawables(biotype=biotype)
+        if not drawables:
+            return None
+        # we order by tracks
+        top = 0
+        space = 0.25
+        annotes = []
+        for feature_type in drawables:
+            new_bottom = top + space
+            for i, annott in enumerate(drawables[feature_type]):
+                annott.shift(y=new_bottom - annott.bottom)
+                if i > 0:
+                    # todo modify the api on annott, we should not be using
+                    # a private attribute!
+                    annott._showlegend = False
+                annotes.append(annott)
+
+            top = annott.top
+
+        top += space
+        height = max((top / len(self)) * width, 300)
+        xaxis = dict(range=[0, len(self)], zeroline=False, showline=True)
+        yaxis = dict(range=[0, top], visible=False, zeroline=True, showline=True)
+
+        if vertical:
+            all_traces = [t.T.as_trace() for t in annotes]
+            width, height = height, width
+            xaxis, yaxis = yaxis, xaxis
+        else:
+            all_traces = [t.as_trace() for t in annotes]
+
+        drawer = Drawable(
+            title=self.name, traces=all_traces, width=width, height=height
+        )
+        drawer.layout.update(xaxis=xaxis, yaxis=yaxis)
+        return drawer
+
     def add_feature(
         self,
         *,
diff --git a/src/cogent3/core/sequence.py b/src/cogent3/core/sequence.py
index 0d2847103..8c1c4e972 100644
--- a/src/cogent3/core/sequence.py
+++ b/src/cogent3/core/sequence.py
@@ -23,7 +23,7 @@ from collections import defaultdict
 from functools import singledispatch, total_ordering
 from operator import eq, ne
 from random import shuffle
-from typing import Generator, List, Optional, Tuple
+from typing import Generator, Iterable, List, Optional, Tuple
 
 from numpy import (
     arange,
@@ -1067,8 +1067,7 @@ class Sequence(SequenceI):
                 new[new < 0] = 0
             elif coord.min() < len(self) < coord.max():
                 new[new > len(self)] = len(self)
-
-            if coord[0] == coord[1]:
+            elif coord[0] == coord[1] or coord.min() > len(self) or coord.max() < 0:
                 # would really to adjust pre and post to be up to the next span
                 continue
             new_spans.append(new.tolist())
@@ -1555,18 +1554,46 @@ class Sequence(SequenceI):
         # annotations are dropped in this case
         return new_seq
 
-    def get_drawables(self):
-        """returns a dict of drawables, keyed by type"""
+    def get_drawables(self, *, biotype: Optional[str, Iterable[str]] = None) -> dict:
+        """returns a dict of drawables, keyed by type
+
+        Parameters
+        ----------
+        biotype
+            passed to get_features(biotype). Can be a single biotype or
+            series. Only features matching this will be included.
+        """
         result = defaultdict(list)
-        for f in self.get_features():
+        for f in self.get_features(biotype=biotype, allow_partial=True):
             result[f.biotype].append(f.get_drawable())
         return result
 
-    def get_drawable(self, width=600, vertical=False):
-        """returns Drawable instance"""
+    def get_drawable(
+        self,
+        *,
+        biotype: Optional[str, Iterable[str]] = None,
+        width: int = 600,
+        vertical: bool = False,
+    ):
+        """make a figure from sequence features
+
+        Parameters
+        ----------
+        biotype
+            passed to get_features(biotype). Can be a single biotype or
+            series. Only features matching this will be included.
+        width
+            width in pixels
+        vertical
+            rotates the drawable
+
+        Returns
+        -------
+        a Drawable instance
+        """
         from cogent3.draw.drawable import Drawable
 
-        drawables = self.get_drawables()
+        drawables = self.get_drawables(biotype=biotype)
         if not drawables:
             return None
         # we order by tracks
diff --git a/src/cogent3/draw/drawable.py b/src/cogent3/draw/drawable.py
index 10c190886..6cb4f2c8c 100644
--- a/src/cogent3/draw/drawable.py
+++ b/src/cogent3/draw/drawable.py
@@ -746,17 +746,19 @@ class _MakeShape:
     """container class that builds annotation shapes"""
 
     _colors = dict(
-        cds="rgba(0,0,150,0.5)",
-        exon="rgba(0,0,100,0.5)",
-        gene="rgba(0,0,150,0.5)",
-        transcript="rgba(0,0,200,0.5)",
-        snp="rgba(200,0,0,0.5)",
-        snv="rgba(200,0,0,0.5)",
+        cds="rgba(0,0,150,0.75)",
+        exon="rgba(0,0,100,0.75)",
+        gene="rgba(161,0,0,0.75)",
+        transcript="rgba(140,102,139,0.75)",
+        mrna="rgba(140,102,139,0.75)",
+        snp="rgba(200,0,0,0.75)",
+        snv="rgba(200,0,0,0.75)",
     )
     _shapes = dict(
         cds=Arrow,
         exon=Arrow,
         transcript=Arrow,
+        mrna=Arrow,
         gene=Arrow,
         repeat=Rectangle,
         snp=Point,
@@ -769,7 +771,7 @@ class _MakeShape:
             if not type_.map.useful:
                 return None
 
-            name = type_.name
+            name = type_.name if type_.map.complete else f"{type_.name} (incomplete)"
             coords = type_.map.get_coordinates()
             reverse = type_.map.get_covering_span().reverse
             type_ = type_.biotype


Test patch:
diff --git a/tests/test_core/test_sequence.py b/tests/test_core/test_sequence.py
index 53d2460be..371a00e9d 100644
--- a/tests/test_core/test_sequence.py
+++ b/tests/test_core/test_sequence.py
@@ -2372,3 +2372,19 @@ def test_sliced_seqview_rich_dict(reverse):
     rd = sv.to_rich_dict()
     assert rd["init_args"]["seq"] == parent[sl]
     assert rd["offset"] == 2
+
+
+def test_get_drawable():
+    seq = cogent3.load_seq(DATADIR / "annotated_seq.gb")
+    seq = seq[2000:4000]
+    biotypes = "CDS", "gene", "mRNA"
+    for feat in seq.get_features(biotype=biotypes, allow_partial=True):
+        draw = feat.get_drawable()
+        assert "(incomplete)" in draw.text
+
+    full = seq.get_drawable(biotype=biotypes)
+    # should only include elements that overlap the segment
+    assert len(full.traces) == len(biotypes)
+    # and their names should indicate they're incomplete
+    for trace in full.traces:
+        assert "(incomplete)" in trace.text