# Copyright 2003-2008 by Leighton Pritchard.  All rights reserved.
# Revisions copyright 2008-2009 by Peter Cock.
#
# This file is part of the Biopython distribution and governed by your
# choice of the "Biopython License Agreement" or the "BSD 3-Clause License".
# Please see the LICENSE file that should have been included as part of this
# package.
#
# Contact:       Leighton Pritchard, The James Hutton Institute,
#                Invergowrie, Dundee, Scotland, DD2 5DA, UK
#                Leighton.Pritchard@hutton.ac.uk
################################################################################

"""Linear Drawer module.

Provides:
 - LinearDrawer -  Drawing object for linear diagrams

For drawing capabilities, this module uses reportlab to draw and write
the diagram: http://www.reportlab.com
"""

# ReportLab imports

from reportlab.graphics.shapes import Drawing, Line, String, Group, Polygon
from reportlab.lib import colors

# GenomeDiagram imports
from ._AbstractDrawer import AbstractDrawer, draw_box, draw_arrow
from ._AbstractDrawer import draw_cut_corner_box, _stroke_and_fill_colors
from ._AbstractDrawer import intermediate_points, angle2trig, deduplicate
from ._FeatureSet import FeatureSet
from ._GraphSet import GraphSet

from math import ceil


class LinearDrawer(AbstractDrawer):
    """Linear Drawer.

    Inherits from:
     - AbstractDrawer

    Attributes:
     - tracklines    Boolean for whether to draw lines delineating tracks
     - pagesize      Tuple describing the size of the page in pixels
     - x0            Float X co-ord for leftmost point of drawable area
     - xlim          Float X co-ord for rightmost point of drawable area
     - y0            Float Y co-ord for lowest point of drawable area
     - ylim          Float Y co-ord for topmost point of drawable area
     - pagewidth     Float pixel width of drawable area
     - pageheight    Float pixel height of drawable area
     - xcenter       Float X co-ord of center of drawable area
     - ycenter       Float Y co-ord of center of drawable area
     - start         Int, base to start drawing from
     - end           Int, base to stop drawing at
     - length        Int, size of sequence to be drawn
     - fragments     Int, number of fragments into which to divide the
       drawn sequence
     - fragment_size Float (0->1) the proportion of the fragment height to
       draw in
     - track_size    Float (0->1) the proportion of the track height to
       draw in
     - drawing       Drawing canvas
     - drawn_tracks  List of ints denoting which tracks are to be drawn
     - current_track_level   Int denoting which track is currently being
       drawn
     - fragment_height   Float total fragment height in pixels
     - fragment_bases    Int total fragment length in bases
     - fragment_lines    Dictionary of top and bottom y-coords of fragment,
       keyed by fragment number
     - fragment_limits   Dictionary of start and end bases of each fragment,
       keyed by fragment number
     - track_offsets     Dictionary of number of pixels that each track top,
       center and bottom is offset from the base of a fragment, keyed by track
     - cross_track_links List of tuples each with four entries (track A,
       feature A, track B, feature B) to be linked.

    """

    def __init__(
        self,
        parent=None,
        pagesize="A3",
        orientation="landscape",
        x=0.05,
        y=0.05,
        xl=None,
        xr=None,
        yt=None,
        yb=None,
        start=None,
        end=None,
        tracklines=0,
        fragments=10,
        fragment_size=None,
        track_size=0.75,
        cross_track_links=None,
    ):
        """Initialize.

        Arguments:
         - parent    Diagram object containing the data that the drawer draws
         - pagesize  String describing the ISO size of the image, or a tuple
           of pixels
         - orientation   String describing the required orientation of the
           final drawing ('landscape' or 'portrait')
         - x         Float (0->1) describing the relative size of the X
           margins to the page
         - y         Float (0->1) describing the relative size of the Y
           margins to the page
         - xl        Float (0->1) describing the relative size of the left X
           margin to the page (overrides x)
         - xl        Float (0->1) describing the relative size of the left X
           margin to the page (overrides x)
         - xr        Float (0->1) describing the relative size of the right X
           margin to the page (overrides x)
         - yt        Float (0->1) describing the relative size of the top Y
           margin to the page (overrides y)
         - yb        Float (0->1) describing the relative size of the lower Y
           margin to the page (overrides y)
         - start     Int, the position to begin drawing the diagram at
         - end       Int, the position to stop drawing the diagram at
         - tracklines    Boolean flag to show (or not) lines delineating tracks
           on the diagram
         - fragments Int, the number of equal fragments into which the
           sequence should be divided for drawing
         - fragment_size Float(0->1) The proportion of the available height
           for the fragment that should be taken up in drawing
         - track_size    The proportion of the available track height that
           should be taken up in drawing
         - cross_track_links List of tuples each with four entries (track A,
           feature A, track B, feature B) to be linked.
        """
        # Use the superclass' instantiation method
        AbstractDrawer.__init__(
            self,
            parent,
            pagesize,
            orientation,
            x,
            y,
            xl,
            xr,
            yt,
            yb,
            start,
            end,
            tracklines,
            cross_track_links,
        )

        # Useful measurements on the page
        self.fragments = fragments
        if fragment_size is not None:
            self.fragment_size = fragment_size
        else:
            if self.fragments == 1:
                # For single fragments, default to full height
                self.fragment_size = 1
            else:
                # Otherwise keep a 10% gap between fragments
                self.fragment_size = 0.9
        self.track_size = track_size

    def draw(self):
        """Draw a linear diagram of the data in the parent Diagram object."""
        # Instantiate the drawing canvas
        self.drawing = Drawing(self.pagesize[0], self.pagesize[1])

        feature_elements = []  # holds feature elements
        feature_labels = []  # holds feature labels
        greytrack_bgs = []  # holds track background
        greytrack_labels = []  # holds track foreground labels
        scale_axes = []  # holds scale axes
        scale_labels = []  # holds scale axis labels

        # Get the tracks to be drawn
        self.drawn_tracks = self._parent.get_drawn_levels()

        # Set fragment and track sizes
        self.init_fragments()
        self.set_track_heights()

        # Go through each track in the parent (if it is to be drawn) one by
        # one and collate the data as drawing elements
        for track_level in self.drawn_tracks:  # only use tracks to be drawn
            self.current_track_level = track_level  # establish track level
            track = self._parent[track_level]  # get the track at that level
            gbgs, glabels = self.draw_greytrack(track)  # get greytrack elements
            greytrack_bgs.append(gbgs)
            greytrack_labels.append(glabels)
            features, flabels = self.draw_track(track)  # get feature and graph elements
            feature_elements.append(features)
            feature_labels.append(flabels)
            if track.scale:
                axes, slabels = self.draw_scale(track)  # get scale elements
                scale_axes.append(axes)
                scale_labels.append(slabels)

        feature_cross_links = []
        for cross_link_obj in self.cross_track_links:
            cross_link_elements = self.draw_cross_link(cross_link_obj)
            if cross_link_elements:
                feature_cross_links.append(cross_link_elements)

        # Groups listed in order of addition to page (from back to front)
        # Draw track backgrounds
        # Draw feature cross track links
        # Draw features and graphs
        # Draw scale axes
        # Draw scale labels
        # Draw feature labels
        # Draw track labels
        element_groups = [
            greytrack_bgs,
            feature_cross_links,
            feature_elements,
            scale_axes,
            scale_labels,
            feature_labels,
            greytrack_labels,
        ]
        for element_group in element_groups:
            for element_list in element_group:
                [self.drawing.add(element) for element in element_list]

        if self.tracklines:  # Draw test tracks over top of diagram
            self.draw_test_tracks()

    def init_fragments(self):
        """Initialize useful values for positioning diagram elements."""
        # Set basic heights, lengths etc
        self.fragment_height = self.pageheight / self.fragments
        # total fragment height in pixels
        self.fragment_bases = ceil(self.length / self.fragments)
        # fragment length in bases

        # Key fragment base and top lines by fragment number
        # Holds bottom and top line locations of fragments, keyed by fragment number
        self.fragment_lines = {}
        # Number of pixels to crop the fragment:
        fragment_crop = (1 - self.fragment_size) / 2
        fragy = self.ylim  # Holder for current absolute fragment base
        for fragment in range(self.fragments):
            fragtop = fragy - fragment_crop * self.fragment_height  # top - crop
            fragbtm = (
                fragy - (1 - fragment_crop) * self.fragment_height
            )  # bottom + crop
            self.fragment_lines[fragment] = (fragbtm, fragtop)
            fragy -= self.fragment_height  # next fragment base

        # Key base starts and ends for each fragment by fragment number
        self.fragment_limits = {}  # Holds first and last base positions in a fragment
        fragment_step = self.fragment_bases  # bases per fragment
        fragment_count = 0
        # Add start and end positions for each fragment to dictionary
        for marker in range(int(self.start), int(self.end), int(fragment_step)):
            self.fragment_limits[fragment_count] = (marker, marker + fragment_step)
            fragment_count += 1

    def set_track_heights(self):
        """Set track heights.

        Since tracks may not be of identical heights, the bottom and top
        offsets of each track relative to the fragment top and bottom is
        stored in a dictionary - self.track_offsets, keyed by track number.
        """
        bot_track = min(min(self.drawn_tracks), 1)
        top_track = max(self.drawn_tracks)  # The 'highest' track number to draw

        trackunit_sum = 0  # Total number of 'units' for the tracks
        trackunits = {}  # The start and end units for each track, keyed by track number
        heightholder = 0  # placeholder variable
        for track in range(bot_track, top_track + 1):  # for all track numbers to 'draw'
            try:
                trackheight = self._parent[track].height  # Get track height
            except Exception:  # TODO: IndexError?
                trackheight = 1  # ...or default to 1
            trackunit_sum += trackheight  # increment total track unit height
            trackunits[track] = (heightholder, heightholder + trackheight)
            heightholder += trackheight  # move to next height
        trackunit_height = self.fragment_height * self.fragment_size / trackunit_sum

        # Calculate top and bottom offsets for each track, relative to fragment
        # base
        track_offsets = {}  # The offsets from fragment base for each track
        track_crop = (
            trackunit_height * (1 - self.track_size) / 2.0
        )  # 'step back' in pixels
        assert track_crop >= 0
        for track in trackunits:
            top = trackunits[track][1] * trackunit_height - track_crop  # top offset
            btm = trackunits[track][0] * trackunit_height + track_crop  # bottom offset
            ctr = btm + (top - btm) / 2.0  # center offset
            track_offsets[track] = (btm, ctr, top)
        self.track_offsets = track_offsets

    def draw_test_tracks(self):
        """Draw test tracks.

        Draw red lines indicating the top and bottom of each fragment,
        and blue ones indicating tracks to be drawn.
        """
        # Add lines for each fragment
        for fbtm, ftop in self.fragment_lines.values():
            self.drawing.add(
                Line(self.x0, ftop, self.xlim, ftop, strokeColor=colors.red)
            )  # top line
            self.drawing.add(
                Line(self.x0, fbtm, self.xlim, fbtm, strokeColor=colors.red)
            )  # bottom line

            # Add track lines for this fragment - but only for drawn tracks
            for track in self.drawn_tracks:
                trackbtm = fbtm + self.track_offsets[track][0]
                trackctr = fbtm + self.track_offsets[track][1]
                tracktop = fbtm + self.track_offsets[track][2]
                self.drawing.add(
                    Line(
                        self.x0, tracktop, self.xlim, tracktop, strokeColor=colors.blue
                    )
                )  # top line
                self.drawing.add(
                    Line(
                        self.x0, trackctr, self.xlim, trackctr, strokeColor=colors.green
                    )
                )  # center line
                self.drawing.add(
                    Line(
                        self.x0, trackbtm, self.xlim, trackbtm, strokeColor=colors.blue
                    )
                )  # bottom line

    def draw_track(self, track):
        """Draw track.

        Arguments:
         - track     Track object

        Returns a tuple (list of elements in the track, list of labels in
        the track).
        """
        track_elements = []  # Holds elements from features and graphs
        track_labels = []  # Holds labels from features and graphs

        # Distribution dictionary for dealing with different set types
        set_methods = {FeatureSet: self.draw_feature_set, GraphSet: self.draw_graph_set}

        for set in track.get_sets():  # Draw the feature or graph sets
            elements, labels = set_methods[set.__class__](set)
            track_elements += elements
            track_labels += labels
        return track_elements, track_labels

    def draw_tick(self, tickpos, ctr, ticklen, track, draw_label):
        """Draw tick.

        Arguments:
         - tickpos   Int, position of the tick on the sequence
         - ctr       Float, Y co-ord of the center of the track
         - ticklen   How long to draw the tick
         - track     Track, the track the tick is drawn on
         - draw_label    Boolean, write the tick label?

        Returns a drawing element that is the tick on the scale
        """
        if self.start >= tickpos and tickpos >= self.end:
            raise RuntimeError(
                "Tick at %i, but showing %i to %i" % (tickpos, self.start, self.end)
            )
        if not (
            (track.start is None or track.start <= tickpos)
            and (track.end is None or tickpos <= track.end)
        ):
            raise RuntimeError(
                "Tick at %i, but showing %r to %r for track"
                % (tickpos, track.start, track.end)
            )
        fragment, tickx = self.canvas_location(tickpos)  # Tick coordinates
        assert fragment >= 0, "Fragment %i, tickpos %i" % (fragment, tickpos)
        tctr = ctr + self.fragment_lines[fragment][0]  # Center line of the track
        tickx += self.x0  # Tick X co-ord
        ticktop = tctr + ticklen  # Y co-ord of tick top
        tick = Line(tickx, tctr, tickx, ticktop, strokeColor=track.scale_color)
        if draw_label:  # Put tick position on as label
            if track.scale_format == "SInt":
                if tickpos >= 1000000:
                    tickstring = str(tickpos // 1000000) + " Mbp"
                elif tickpos >= 1000:
                    tickstring = str(tickpos // 1000) + " Kbp"
                else:
                    tickstring = str(tickpos)
            else:
                tickstring = str(tickpos)
            label = String(
                0,
                0,
                tickstring,  # Make label string
                fontName=track.scale_font,
                fontSize=track.scale_fontsize,
                fillColor=track.scale_color,
            )
            labelgroup = Group(label)
            rotation = angle2trig(track.scale_fontangle)
            labelgroup.transform = (
                rotation[0],
                rotation[1],
                rotation[2],
                rotation[3],
                tickx,
                ticktop,
            )
        else:
            labelgroup = None
        return tick, labelgroup

    def draw_scale(self, track):
        """Draw scale.

        Argument:
         - track     Track object

        Returns a tuple of (list of elements in the scale, list of labels
        in the scale).
        """
        scale_elements = []  # Holds axes and ticks
        scale_labels = []  # Holds labels

        if not track.scale:  # No scale required, exit early
            return [], []

        # Get track location
        btm, ctr, top = self.track_offsets[self.current_track_level]
        trackheight = top - ctr

        # For each fragment, draw the scale for this track
        start, end = self._current_track_start_end()
        start_f, start_x = self.canvas_location(start)
        end_f, end_x = self.canvas_location(end)

        for fragment in range(start_f, end_f + 1):
            tbtm = btm + self.fragment_lines[fragment][0]
            tctr = ctr + self.fragment_lines[fragment][0]
            ttop = top + self.fragment_lines[fragment][0]
            # X-axis
            if fragment == start_f:
                x_left = start_x
            else:
                x_left = 0
            if fragment == end_f:
                x_right = end_x
                # Y-axis end marker
                scale_elements.append(
                    Line(
                        self.x0 + x_right,
                        tbtm,
                        self.x0 + x_right,
                        ttop,
                        strokeColor=track.scale_color,
                    )
                )
            else:
                x_right = self.xlim - self.x0
            scale_elements.append(
                Line(
                    self.x0 + x_left,
                    tctr,
                    self.x0 + x_right,
                    tctr,
                    strokeColor=track.scale_color,
                )
            )
            # Y-axis start marker
            scale_elements.append(
                Line(
                    self.x0 + x_left,
                    tbtm,
                    self.x0 + x_left,
                    ttop,
                    strokeColor=track.scale_color,
                )
            )

        start, end = self._current_track_start_end()
        if track.scale_ticks:  # Ticks are required on the scale
            # Draw large ticks
            # I want the ticks to be consistently positioned relative to
            # the start of the sequence (position 0), not relative to the
            # current viewpoint (self.start and self.end)

            ticklen = track.scale_largeticks * trackheight
            tickiterval = int(track.scale_largetick_interval)
            # Note that we could just start the list of ticks using
            # range(0,self.end,tickinterval) and the filter out the
            # ones before self.start - but this seems wasteful.
            # Using tickiterval * (self.start//tickiterval) is a shortcut.
            for tickpos in range(
                tickiterval * (self.start // tickiterval), int(self.end), tickiterval
            ):
                if tickpos <= start or end <= tickpos:
                    continue
                tick, label = self.draw_tick(
                    tickpos, ctr, ticklen, track, track.scale_largetick_labels
                )
                scale_elements.append(tick)
                if label is not None:  # If there's a label, add it
                    scale_labels.append(label)
            # Draw small ticks
            ticklen = track.scale_smallticks * trackheight
            tickiterval = int(track.scale_smalltick_interval)
            for tickpos in range(
                tickiterval * (self.start // tickiterval), int(self.end), tickiterval
            ):
                if tickpos <= start or end <= tickpos:
                    continue
                tick, label = self.draw_tick(
                    tickpos, ctr, ticklen, track, track.scale_smalltick_labels
                )
                scale_elements.append(tick)
                if label is not None:  # If there's a label, add it
                    scale_labels.append(label)

        # Check to see if the track contains a graph - if it does, get the
        # minimum and maximum values, and put them on the scale Y-axis
        if track.axis_labels:
            for set in track.get_sets():  # Check all sets...
                if set.__class__ is GraphSet:  # ...for a graph set
                    graph_label_min = []
                    graph_label_mid = []
                    graph_label_max = []
                    for graph in set.get_graphs():
                        quartiles = graph.quartiles()
                        minval, maxval = quartiles[0], quartiles[4]
                        if graph.center is None:
                            midval = (maxval + minval) / 2.0
                            graph_label_min.append(f"{minval:.3f}")
                            graph_label_max.append(f"{maxval:.3f}")
                        else:
                            diff = max((graph.center - minval), (maxval - graph.center))
                            minval = graph.center - diff
                            maxval = graph.center + diff
                            midval = graph.center
                            graph_label_mid.append(f"{midval:.3f}")
                            graph_label_min.append(f"{minval:.3f}")
                            graph_label_max.append(f"{maxval:.3f}")
                    for fragment in range(
                        start_f, end_f + 1
                    ):  # Add to all used fragment axes
                        tbtm = btm + self.fragment_lines[fragment][0]
                        tctr = ctr + self.fragment_lines[fragment][0]
                        ttop = top + self.fragment_lines[fragment][0]
                        if fragment == start_f:
                            x_left = start_x
                        else:
                            x_left = 0
                        for val, pos in [
                            (";".join(graph_label_min), tbtm),
                            (";".join(graph_label_max), ttop),
                            (";".join(graph_label_mid), tctr),
                        ]:
                            label = String(
                                0,
                                0,
                                val,
                                fontName=track.scale_font,
                                fontSize=track.scale_fontsize,
                                fillColor=track.scale_color,
                            )
                            labelgroup = Group(label)
                            rotation = angle2trig(track.scale_fontangle)
                            labelgroup.transform = (
                                rotation[0],
                                rotation[1],
                                rotation[2],
                                rotation[3],
                                self.x0 + x_left,
                                pos,
                            )
                            scale_labels.append(labelgroup)

        return scale_elements, scale_labels

    def draw_greytrack(self, track):
        """Draw greytrack.

        Arguments:
         - track     Track object

        Put in a grey background to the current track in all fragments,
        if track specifies that we should.
        """
        greytrack_bgs = []  # Holds grey track backgrounds
        greytrack_labels = []  # Holds grey foreground labels

        if not track.greytrack:  # No greytrack required, return early
            return [], []

        # Get track location
        btm, ctr, top = self.track_offsets[self.current_track_level]

        start, end = self._current_track_start_end()
        start_fragment, start_offset = self.canvas_location(start)
        end_fragment, end_offset = self.canvas_location(end)

        # Add greytrack to all fragments for this track
        for fragment in range(start_fragment, end_fragment + 1):
            tbtm = btm + self.fragment_lines[fragment][0]
            tctr = ctr + self.fragment_lines[fragment][0]
            ttop = top + self.fragment_lines[fragment][0]
            if fragment == start_fragment:
                x1 = self.x0 + start_offset
            else:
                x1 = self.x0
            if fragment == end_fragment:
                x2 = self.x0 + end_offset
            else:
                x2 = self.xlim
            box = draw_box(
                (x1, tbtm),
                (x2, ttop),
                colors.Color(0.96, 0.96, 0.96),  # Grey track bg
            )  # is just a box
            greytrack_bgs.append(box)

            if track.greytrack_labels:  # If labels are required
                # # how far apart should they be?
                labelstep = self.pagewidth / track.greytrack_labels
                label = String(
                    0,
                    0,
                    track.name,  # label contents
                    fontName=track.greytrack_font,
                    fontSize=track.greytrack_fontsize,
                    fillColor=track.greytrack_fontcolor,
                )
                # Create a new labelgroup at each position the label is required
                for x in range(int(self.x0), int(self.xlim), int(labelstep)):
                    if fragment == start_fragment and x < start_offset:
                        continue
                    if (
                        fragment == end_fragment
                        and end_offset < x + label.getBounds()[2]
                    ):
                        continue
                    labelgroup = Group(label)
                    rotation = angle2trig(track.greytrack_font_rotation)
                    labelgroup.transform = (
                        rotation[0],
                        rotation[1],
                        rotation[2],
                        rotation[3],
                        x,
                        tbtm,
                    )
                    if not self.xlim - x <= labelstep:
                        # Don't overlap the end of the track
                        greytrack_labels.append(labelgroup)

        return greytrack_bgs, greytrack_labels

    def draw_feature_set(self, set):
        """Draw feature set.

        Arguments:
         - set       FeatureSet object

        Returns a tuple (list of elements describing features, list of
        labels for elements).
        """
        # print("draw feature set")
        feature_elements = []  # Holds diagram elements belonging to the features
        label_elements = []  # Holds diagram elements belonging to feature labels

        # Collect all the elements for the feature set
        for feature in set.get_features():
            if self.is_in_bounds(feature.start) or self.is_in_bounds(feature.end):
                features, labels = self.draw_feature(feature)  # get elements and labels
                feature_elements += features
                label_elements += labels

        return feature_elements, label_elements

    def draw_feature(self, feature):
        """Draw feature.

        Arguments:
         - feature           Feature containing location info

        Returns tuple of (list of elements describing single feature, list
        of labels for those elements).
        """
        if feature.hide:  # Feature hidden, don't draw it...
            return [], []

        feature_elements = []  # Holds diagram elements belonging to the feature
        label_elements = []  # Holds labels belonging to the feature

        start, end = self._current_track_start_end()
        # A single feature may be split into subfeatures, so loop over them
        for locstart, locend in feature.locations:
            if locend < start:
                continue
            locstart = max(locstart, start)
            if end < locstart:
                continue
            locend = min(locend, end)
            feature_boxes = self.draw_feature_location(feature, locstart, locend)
            for box, label in feature_boxes:
                feature_elements.append(box)
                if label is not None:
                    label_elements.append(label)

        return feature_elements, label_elements

    def draw_feature_location(self, feature, locstart, locend):
        """Draw feature location."""
        feature_boxes = []
        # Get start and end positions for feature/subfeatures
        start_fragment, start_offset = self.canvas_location(locstart)
        end_fragment, end_offset = self.canvas_location(locend)
        # print("start_fragment, start_offset", start_fragment, start_offset)
        # print("end_fragment, end_offset", end_fragment, end_offset)
        # print("start, end", locstart, locend)

        # Note that there is a strange situation where a feature may be in
        # several parts, and one or more of those parts may end up being
        # drawn on a non-existent fragment.  So we check that the start and
        # end fragments do actually exist in terms of the drawing
        allowed_fragments = list(self.fragment_limits.keys())
        if start_fragment in allowed_fragments and end_fragment in allowed_fragments:
            # print(feature.name, feature.start, feature.end, start_offset, end_offset)
            if start_fragment == end_fragment:  # Feature is found on one fragment
                feature_box, label = self.get_feature_sigil(
                    feature, start_offset, end_offset, start_fragment
                )
                feature_boxes.append((feature_box, label))
                # feature_elements.append(feature_box)
                # if label is not None:   # There is a label for the feature
                #    label_elements.append(label)
            else:  # Feature is split over two or more fragments
                fragment = start_fragment
                start = start_offset
                # The bit that runs up to the end of the first fragment,
                # and any bits that subsequently span whole fragments
                while self.fragment_limits[fragment][1] < locend:
                    # print(fragment, self.fragment_limits[fragment][1], locend)
                    feature_box, label = self.get_feature_sigil(
                        feature, start, self.pagewidth, fragment
                    )

                    fragment += 1  # move to next fragment
                    start = 0  # start next sigil from start of fragment
                    feature_boxes.append((feature_box, label))
                    # feature_elements.append(feature_box)
                    # if label is not None:   # There's a label for the feature
                    #    label_elements.append(label)
                # The last bit of the feature
                # print(locend, self.end, fragment)
                # print(self.fragment_bases, self.length)
                feature_box, label = self.get_feature_sigil(
                    feature, 0, end_offset, fragment
                )
                feature_boxes.append((feature_box, label))
        # if locstart > locend:
        #    print(locstart, locend, feature.location.strand, feature_boxes, feature.name)
        return feature_boxes

    def draw_cross_link(self, cross_link):
        """Draw cross-link between two features."""
        startA = cross_link.startA
        startB = cross_link.startB
        endA = cross_link.endA
        endB = cross_link.endB

        if not self.is_in_bounds(startA) and not self.is_in_bounds(endA):
            return None
        if not self.is_in_bounds(startB) and not self.is_in_bounds(endB):
            return None

        if startA < self.start:
            startA = self.start
        if startB < self.start:
            startB = self.start
        if self.end < endA:
            endA = self.end
        if self.end < endB:
            endB = self.end

        trackobjA = cross_link._trackA(list(self._parent.tracks.values()))
        trackobjB = cross_link._trackB(list(self._parent.tracks.values()))
        assert trackobjA is not None
        assert trackobjB is not None
        if trackobjA == trackobjB:
            raise NotImplementedError

        if trackobjA.start is not None:
            if endA < trackobjA.start:
                return
            startA = max(startA, trackobjA.start)
        if trackobjA.end is not None:
            if trackobjA.end < startA:
                return
            endA = min(endA, trackobjA.end)
        if trackobjB.start is not None:
            if endB < trackobjB.start:
                return
            startB = max(startB, trackobjB.start)
        if trackobjB.end is not None:
            if trackobjB.end < startB:
                return
            endB = min(endB, trackobjB.end)

        for track_level in self._parent.get_drawn_levels():
            track = self._parent[track_level]
            if track == trackobjA:
                trackA = track_level
            if track == trackobjB:
                trackB = track_level
        if trackA == trackB:
            raise NotImplementedError

        strokecolor, fillcolor = _stroke_and_fill_colors(
            cross_link.color, cross_link.border
        )

        allowed_fragments = list(self.fragment_limits.keys())

        start_fragmentA, start_offsetA = self.canvas_location(startA)
        end_fragmentA, end_offsetA = self.canvas_location(endA)
        if (
            start_fragmentA not in allowed_fragments
            or end_fragmentA not in allowed_fragments
        ):
            return

        start_fragmentB, start_offsetB = self.canvas_location(startB)
        end_fragmentB, end_offsetB = self.canvas_location(endB)
        if (
            start_fragmentB not in allowed_fragments
            or end_fragmentB not in allowed_fragments
        ):
            return

        # TODO - Better drawing of flips when split between fragments

        answer = []
        for fragment in range(
            min(start_fragmentA, start_fragmentB), max(end_fragmentA, end_fragmentB) + 1
        ):
            btmA, ctrA, topA = self.track_offsets[trackA]
            btmA += self.fragment_lines[fragment][0]
            ctrA += self.fragment_lines[fragment][0]
            topA += self.fragment_lines[fragment][0]

            btmB, ctrB, topB = self.track_offsets[trackB]
            btmB += self.fragment_lines[fragment][0]
            ctrB += self.fragment_lines[fragment][0]
            topB += self.fragment_lines[fragment][0]

            if self.fragment_limits[fragment][1] < endA:
                xAe = self.x0 + self.pagewidth
                crop_rightA = True
            else:
                xAe = self.x0 + end_offsetA
                crop_rightA = False
            if self.fragment_limits[fragment][1] < endB:
                xBe = self.x0 + self.pagewidth
                crop_rightB = True
            else:
                xBe = self.x0 + end_offsetB
                crop_rightB = False

            if fragment < start_fragmentA:
                xAs = self.x0 + self.pagewidth
                xAe = xAs
                crop_leftA = False
            elif fragment == start_fragmentA:
                xAs = self.x0 + start_offsetA
                crop_leftA = False
            else:
                xAs = self.x0
                crop_leftA = True

            if fragment < start_fragmentB:
                xBs = self.x0 + self.pagewidth
                xBe = xBs
                crop_leftB = False
            elif fragment == start_fragmentB:
                xBs = self.x0 + start_offsetB
                crop_leftB = False
            else:
                xBs = self.x0
                crop_leftB = True

            if ctrA < ctrB:
                yA = topA
                yB = btmB
            else:
                yA = btmA
                yB = topB

            if fragment < start_fragmentB or end_fragmentB < fragment:
                if cross_link.flip:
                    # Just draw A as a triangle to left/right
                    if fragment < start_fragmentB:
                        extra = [self.x0 + self.pagewidth, 0.5 * (yA + yB)]
                    else:
                        extra = [self.x0, 0.5 * (yA + yB)]
                else:
                    if fragment < start_fragmentB:
                        extra = [
                            self.x0 + self.pagewidth,
                            0.7 * yA + 0.3 * yB,
                            self.x0 + self.pagewidth,
                            0.3 * yA + 0.7 * yB,
                        ]
                    else:
                        extra = [
                            self.x0,
                            0.3 * yA + 0.7 * yB,
                            self.x0,
                            0.7 * yA + 0.3 * yB,
                        ]
                answer.append(
                    Polygon(
                        deduplicate([xAs, yA, xAe, yA] + extra),
                        strokeColor=strokecolor,
                        fillColor=fillcolor,
                        # default is mitre/miter which can stick out too much:
                        strokeLineJoin=1,  # 1=round
                        strokewidth=0,
                    )
                )
            elif fragment < start_fragmentA or end_fragmentA < fragment:
                if cross_link.flip:
                    # Just draw B as a triangle to left
                    if fragment < start_fragmentA:
                        extra = [self.x0 + self.pagewidth, 0.5 * (yA + yB)]
                    else:
                        extra = [self.x0, 0.5 * (yA + yB)]
                else:
                    if fragment < start_fragmentA:
                        extra = [
                            self.x0 + self.pagewidth,
                            0.3 * yA + 0.7 * yB,
                            self.x0 + self.pagewidth,
                            0.7 * yA + 0.3 * yB,
                        ]
                    else:
                        extra = [
                            self.x0,
                            0.7 * yA + 0.3 * yB,
                            self.x0,
                            0.3 * yA + 0.7 * yB,
                        ]
                answer.append(
                    Polygon(
                        deduplicate([xBs, yB, xBe, yB] + extra),
                        strokeColor=strokecolor,
                        fillColor=fillcolor,
                        # default is mitre/miter which can stick out too much:
                        strokeLineJoin=1,  # 1=round
                        strokewidth=0,
                    )
                )
            elif cross_link.flip and (
                (crop_leftA and not crop_rightA) or (crop_leftB and not crop_rightB)
            ):
                # On left end of fragment... force "crossing" to margin
                answer.append(
                    Polygon(
                        deduplicate(
                            [
                                xAs,
                                yA,
                                xAe,
                                yA,
                                self.x0,
                                0.5 * (yA + yB),
                                xBe,
                                yB,
                                xBs,
                                yB,
                            ]
                        ),
                        strokeColor=strokecolor,
                        fillColor=fillcolor,
                        # default is mitre/miter which can stick out too much:
                        strokeLineJoin=1,  # 1=round
                        strokewidth=0,
                    )
                )
            elif cross_link.flip and (
                (crop_rightA and not crop_leftA) or (crop_rightB and not crop_leftB)
            ):
                # On right end... force "crossing" to margin
                answer.append(
                    Polygon(
                        deduplicate(
                            [
                                xAs,
                                yA,
                                xAe,
                                yA,
                                xBe,
                                yB,
                                xBs,
                                yB,
                                self.x0 + self.pagewidth,
                                0.5 * (yA + yB),
                            ]
                        ),
                        strokeColor=strokecolor,
                        fillColor=fillcolor,
                        # default is mitre/miter which can stick out too much:
                        strokeLineJoin=1,  # 1=round
                        strokewidth=0,
                    )
                )
            elif cross_link.flip:
                answer.append(
                    Polygon(
                        deduplicate([xAs, yA, xAe, yA, xBs, yB, xBe, yB]),
                        strokeColor=strokecolor,
                        fillColor=fillcolor,
                        # default is mitre/miter which can stick out too much:
                        strokeLineJoin=1,  # 1=round
                        strokewidth=0,
                    )
                )
            else:
                answer.append(
                    Polygon(
                        deduplicate([xAs, yA, xAe, yA, xBe, yB, xBs, yB]),
                        strokeColor=strokecolor,
                        fillColor=fillcolor,
                        # default is mitre/miter which can stick out too much:
                        strokeLineJoin=1,  # 1=round
                        strokewidth=0,
                    )
                )
        return answer

    def get_feature_sigil(self, feature, x0, x1, fragment, **kwargs):
        """Get feature sigil.

        Arguments:
         - feature       Feature object
         - x0            Start X coordinate on diagram
         - x1            End X coordinate on diagram
         - fragment      The fragment on which the feature appears

        Returns a drawable indicator of the feature, and any required label
        for it.
        """
        # Establish coordinates for drawing
        x0, x1 = self.x0 + x0, self.x0 + x1
        btm, ctr, top = self.track_offsets[self.current_track_level]
        try:
            btm += self.fragment_lines[fragment][0]
            ctr += self.fragment_lines[fragment][0]
            top += self.fragment_lines[fragment][0]
        except Exception:  # Only called if the method screws up big time
            print("We've got a screw-up")
            print(f"{self.start} {self.end}")
            print(self.fragment_bases)
            print(f"{x0!r} {x1!r}")
            for locstart, locend in feature.locations:
                print(self.canvas_location(locstart))
                print(self.canvas_location(locend))
            print(f"FEATURE\n{feature}")
            raise

        # Distribution dictionary for various ways of drawing the feature
        draw_methods = {
            "BOX": self._draw_sigil_box,
            "ARROW": self._draw_sigil_arrow,
            "BIGARROW": self._draw_sigil_big_arrow,
            "OCTO": self._draw_sigil_octo,
            "JAGGY": self._draw_sigil_jaggy,
        }

        method = draw_methods[feature.sigil]
        kwargs["head_length_ratio"] = feature.arrowhead_length
        kwargs["shaft_height_ratio"] = feature.arrowshaft_height

        # Support for clickable links... needs ReportLab 2.4 or later
        # which added support for links in SVG output.
        if hasattr(feature, "url"):
            kwargs["hrefURL"] = feature.url
            kwargs["hrefTitle"] = feature.name

        # Get sigil for the feature, give it the bounding box straddling
        # the axis (it decides strand specific placement)
        sigil = method(
            btm,
            ctr,
            top,
            x0,
            x1,
            strand=feature.location.strand,
            color=feature.color,
            border=feature.border,
            **kwargs,
        )

        if feature.label_strand:
            strand = feature.label_strand
        else:
            strand = feature.location.strand
        if feature.label:  # Feature requires a label
            label = String(
                0,
                0,
                feature.name,
                fontName=feature.label_font,
                fontSize=feature.label_size,
                fillColor=feature.label_color,
            )
            labelgroup = Group(label)
            # Feature is on top, or covers both strands (location affects
            # the height and rotation of the label)
            if strand != -1:
                rotation = angle2trig(feature.label_angle)
                if feature.label_position in ("end", "3'", "right"):
                    pos = x1
                elif feature.label_position in ("middle", "center", "centre"):
                    pos = (x1 + x0) / 2.0
                else:
                    # Default to start, i.e. 'start', "5'", 'left'
                    pos = x0
                labelgroup.transform = (
                    rotation[0],
                    rotation[1],
                    rotation[2],
                    rotation[3],
                    pos,
                    top,
                )
            else:  # Feature on bottom strand
                rotation = angle2trig(feature.label_angle + 180)
                if feature.label_position in ("end", "3'", "right"):
                    pos = x0
                elif feature.label_position in ("middle", "center", "centre"):
                    pos = (x1 + x0) / 2.0
                else:
                    # Default to start, i.e. 'start', "5'", 'left'
                    pos = x1
                labelgroup.transform = (
                    rotation[0],
                    rotation[1],
                    rotation[2],
                    rotation[3],
                    pos,
                    btm,
                )
        else:
            labelgroup = None
        return sigil, labelgroup

    def draw_graph_set(self, set):
        """Draw graph set.

        Arguments:
         - set       GraphSet object

        Returns tuple (list of graph elements, list of graph labels).
        """
        # print('draw graph set')
        elements = []  # Holds graph elements

        # Distribution dictionary for how to draw the graph
        style_methods = {
            "line": self.draw_line_graph,
            "heat": self.draw_heat_graph,
            "bar": self.draw_bar_graph,
        }

        for graph in set.get_graphs():
            elements += style_methods[graph.style](graph)

        return elements, []

    def draw_line_graph(self, graph):
        """Return a line graph as a list of drawable elements.

        Arguments:
         - graph     Graph object

        """
        # print('\tdraw_line_graph')
        line_elements = []  # Holds drawable elements

        # Get graph data
        data_quartiles = graph.quartiles()
        minval, maxval = data_quartiles[0], data_quartiles[4]
        btm, ctr, top = self.track_offsets[self.current_track_level]
        trackheight = 0.5 * (top - btm)
        datarange = maxval - minval
        if datarange == 0:
            datarange = trackheight

        start, end = self._current_track_start_end()
        data = graph[start:end]

        # midval is the value at which the x-axis is plotted, and is the
        # central ring in the track
        if graph.center is None:
            midval = (maxval + minval) / 2.0
        else:
            midval = graph.center
        # Whichever is the greatest difference: max-midval or min-midval, is
        # taken to specify the number of pixel units resolved along the
        # y-axis
        resolution = max((midval - minval), (maxval - midval))

        # Start from first data point
        pos, val = data[0]
        lastfrag, lastx = self.canvas_location(pos)
        lastx += self.x0  # Start xy co-ords
        lasty = (
            trackheight * (val - midval) / resolution
            + self.fragment_lines[lastfrag][0]
            + ctr
        )
        lastval = val
        # Add a series of lines linking consecutive data points
        for pos, val in data:
            frag, x = self.canvas_location(pos)
            x += self.x0  # next xy co-ords
            y = (
                trackheight * (val - midval) / resolution
                + self.fragment_lines[frag][0]
                + ctr
            )
            if frag == lastfrag:  # Points on the same fragment: draw the line
                line_elements.append(
                    Line(
                        lastx,
                        lasty,
                        x,
                        y,
                        strokeColor=graph.poscolor,
                        strokeWidth=graph.linewidth,
                    )
                )
            else:  # Points not on the same fragment, so interpolate
                tempy = (
                    trackheight * (val - midval) / resolution
                    + self.fragment_lines[lastfrag][0]
                    + ctr
                )
                line_elements.append(
                    Line(
                        lastx,
                        lasty,
                        self.xlim,
                        tempy,
                        strokeColor=graph.poscolor,
                        strokeWidth=graph.linewidth,
                    )
                )
                tempy = (
                    trackheight * (val - midval) / resolution
                    + self.fragment_lines[frag][0]
                    + ctr
                )
                line_elements.append(
                    Line(
                        self.x0,
                        tempy,
                        x,
                        y,
                        strokeColor=graph.poscolor,
                        strokeWidth=graph.linewidth,
                    )
                )
            lastfrag, lastx, lasty, lastval = frag, x, y, val

        return line_elements

    def draw_heat_graph(self, graph):
        """Return a list of drawable elements for the heat graph."""
        # print('\tdraw_heat_graph')
        # At each point contained in the graph data, we draw a box that is the
        # full height of the track, extending from the midpoint between the
        # previous and current data points to the midpoint between the current
        # and next data points
        heat_elements = []  # Holds drawable elements for the graph

        # Get graph data and information
        data_quartiles = graph.quartiles()
        minval, maxval = data_quartiles[0], data_quartiles[4]
        midval = (maxval + minval) / 2.0  # mid is the value at the X-axis
        btm, ctr, top = self.track_offsets[self.current_track_level]
        trackheight = top - btm

        start, end = self._current_track_start_end()
        data = intermediate_points(start, end, graph[start:end])

        if not data:
            return []

        # Create elements on the graph, indicating a large positive value by
        # the graph's poscolor, and a large negative value by the graph's
        # negcolor attributes
        for pos0, pos1, val in data:
            # assert start <= pos0 <= pos1 <= end
            fragment0, x0 = self.canvas_location(pos0)
            fragment1, x1 = self.canvas_location(pos1)
            x0, x1 = self.x0 + x0, self.x0 + x1  # account for margin
            # print('x1 before:', x1)

            # Calculate the heat color, based on the differential between
            # the value and the median value
            heat = colors.linearlyInterpolatedColor(
                graph.poscolor, graph.negcolor, maxval, minval, val
            )

            # Draw heat box
            if fragment0 == fragment1:  # Box is contiguous on one fragment
                if pos1 >= self.fragment_limits[fragment0][1]:
                    x1 = self.xlim
                ttop = top + self.fragment_lines[fragment0][0]
                tbtm = btm + self.fragment_lines[fragment0][0]
                # print('equal', pos0, pos1, val)
                # print(pos0, pos1, fragment0, fragment1)
                heat_elements.append(
                    draw_box((x0, tbtm), (x1, ttop), color=heat, border=None)
                )
            else:  # box is split over two or more fragments
                # if pos0 >= self.fragment_limits[fragment0][0]:
                #    fragment0 += 1
                fragment = fragment0
                start_x = x0
                while self.fragment_limits[fragment][1] <= pos1:
                    # print(pos0, self.fragment_limits[fragment][1], pos1)
                    ttop = top + self.fragment_lines[fragment][0]
                    tbtm = btm + self.fragment_lines[fragment][0]
                    heat_elements.append(
                        draw_box(
                            (start_x, tbtm), (self.xlim, ttop), color=heat, border=None
                        )
                    )
                    fragment += 1
                    start_x = self.x0
                ttop = top + self.fragment_lines[fragment][0]
                tbtm = btm + self.fragment_lines[fragment][0]
                # Add the last part of the bar
                # print('x1 after:', x1, '\n')
                heat_elements.append(
                    draw_box((self.x0, tbtm), (x1, ttop), color=heat, border=None)
                )

        return heat_elements

    def draw_bar_graph(self, graph):
        """Return list of drawable elements for a bar graph."""
        # print('\tdraw_bar_graph')
        # At each point contained in the graph data, we draw a vertical bar
        # from the track center to the height of the datapoint value (positive
        # values go up in one color, negative go down in the alternative
        # color).
        bar_elements = []  # Holds drawable elements for the graph

        # Set the number of pixels per unit for the data
        data_quartiles = graph.quartiles()
        minval, maxval = data_quartiles[0], data_quartiles[4]
        btm, ctr, top = self.track_offsets[self.current_track_level]
        trackheight = 0.5 * (top - btm)
        datarange = maxval - minval
        if datarange == 0:
            datarange = trackheight
        data = graph[self.start : self.end]
        # midval is the value at which the x-axis is plotted, and is the
        # central ring in the track
        if graph.center is None:
            midval = (maxval + minval) / 2.0
        else:
            midval = graph.center

        # Convert data into 'binned' blocks, covering half the distance to the
        # next data point on either side, accounting for the ends of fragments
        # and tracks
        start, end = self._current_track_start_end()
        data = intermediate_points(start, end, graph[start:end])

        if not data:
            return []

        # Whichever is the greatest difference: max-midval or min-midval, is
        # taken to specify the number of pixel units resolved along the
        # y-axis
        resolution = max((midval - minval), (maxval - midval))
        if resolution == 0:
            resolution = trackheight

        # Create elements for the bar graph based on newdata
        for pos0, pos1, val in data:
            fragment0, x0 = self.canvas_location(pos0)
            fragment1, x1 = self.canvas_location(pos1)
            x0, x1 = self.x0 + x0, self.x0 + x1  # account for margin
            barval = trackheight * (val - midval) / resolution
            if barval >= 0:  # Different colors for bars that extend above...
                barcolor = graph.poscolor
            else:  # ...or below the axis
                barcolor = graph.negcolor

            # Draw bar
            if fragment0 == fragment1:  # Box is contiguous
                if pos1 >= self.fragment_limits[fragment0][1]:
                    x1 = self.xlim
                tctr = ctr + self.fragment_lines[fragment0][0]
                barval += tctr
                bar_elements.append(draw_box((x0, tctr), (x1, barval), color=barcolor))
            else:  # Box is split over two or more fragments
                fragment = fragment0
                # if pos0 >= self.fragment_limits[fragment0][0]:
                #    fragment += 1
                start = x0
                while self.fragment_limits[fragment][1] < pos1:
                    tctr = ctr + self.fragment_lines[fragment][0]
                    thisbarval = barval + tctr
                    bar_elements.append(
                        draw_box((start, tctr), (self.xlim, thisbarval), color=barcolor)
                    )
                    fragment += 1
                    start = self.x0
                tctr = ctr + self.fragment_lines[fragment1][0]
                barval += tctr
                # Add the last part of the bar
                bar_elements.append(
                    draw_box((self.x0, tctr), (x1, barval), color=barcolor)
                )

        return bar_elements

    def canvas_location(self, base):
        """Canvas location of a base on the genome.

        Arguments:
         - base      The base number on the genome sequence

        Returns the x-coordinate and fragment number of a base on the
        genome sequence, in the context of the current drawing setup
        """
        base = int(base - self.start)  # number of bases we are from the start
        fragment = int(base / self.fragment_bases)
        if fragment < 1:  # First fragment
            base_offset = base
            fragment = 0
        elif fragment >= self.fragments:
            fragment = self.fragments - 1
            base_offset = self.fragment_bases
        else:  # Calculate number of bases from start of fragment
            base_offset = base % self.fragment_bases
        assert fragment < self.fragments, (
            base,
            self.start,
            self.end,
            self.length,
            self.fragment_bases,
        )
        # Calculate number of pixels from start of fragment
        x_offset = self.pagewidth * base_offset / self.fragment_bases
        return fragment, x_offset

    def _draw_sigil_box(self, bottom, center, top, x1, x2, strand, **kwargs):
        """Draw BOX sigil (PRIVATE)."""
        if strand == 1:
            y1 = center
            y2 = top
        elif strand == -1:
            y1 = bottom
            y2 = center
        else:
            y1 = bottom
            y2 = top
        return draw_box((x1, y1), (x2, y2), **kwargs)

    def _draw_sigil_octo(self, bottom, center, top, x1, x2, strand, **kwargs):
        """Draw OCTO sigil, a box with the corners cut off (PRIVATE)."""
        if strand == 1:
            y1 = center
            y2 = top
        elif strand == -1:
            y1 = bottom
            y2 = center
        else:
            y1 = bottom
            y2 = top
        return draw_cut_corner_box((x1, y1), (x2, y2), **kwargs)

    def _draw_sigil_jaggy(
        self, bottom, center, top, x1, x2, strand, color, border=None, **kwargs
    ):
        """Draw JAGGY sigil (PRIVATE).

        Although we may in future expose the head/tail jaggy lengths, for now
        both the left and right edges are drawn jagged.
        """
        if strand == 1:
            y1 = center
            y2 = top
            teeth = 2
        elif strand == -1:
            y1 = bottom
            y2 = center
            teeth = 2
        else:
            y1 = bottom
            y2 = top
            teeth = 4

        xmin = min(x1, x2)
        xmax = max(x1, x2)
        height = y2 - y1
        boxwidth = x2 - x1
        tooth_length = min(height / teeth, boxwidth * 0.5)

        headlength = tooth_length
        taillength = tooth_length

        strokecolor, color = _stroke_and_fill_colors(color, border)

        points = []
        for i in range(teeth):
            points.extend(
                (
                    xmin,
                    y1 + i * height / teeth,
                    xmin + taillength,
                    y1 + (i + 1) * height / teeth,
                )
            )
        for i in range(teeth):
            points.extend(
                (
                    xmax,
                    y1 + (teeth - i) * height / teeth,
                    xmax - headlength,
                    y1 + (teeth - i - 1) * height / teeth,
                )
            )

        return Polygon(
            deduplicate(points),
            strokeColor=strokecolor,
            strokeWidth=1,
            strokeLineJoin=1,  # 1=round
            fillColor=color,
            **kwargs,
        )

    def _draw_sigil_arrow(self, bottom, center, top, x1, x2, strand, **kwargs):
        """Draw ARROW sigil (PRIVATE)."""
        if strand == 1:
            y1 = center
            y2 = top
            orientation = "right"
        elif strand == -1:
            y1 = bottom
            y2 = center
            orientation = "left"
        else:
            y1 = bottom
            y2 = top
            orientation = "right"  # backward compatibility
        return draw_arrow((x1, y1), (x2, y2), orientation=orientation, **kwargs)

    def _draw_sigil_big_arrow(self, bottom, center, top, x1, x2, strand, **kwargs):
        """Draw BIGARROW sigil, like ARROW but straddles the axis (PRIVATE)."""
        if strand == -1:
            orientation = "left"
        else:
            orientation = "right"
        return draw_arrow((x1, bottom), (x2, top), orientation=orientation, **kwargs)
