Source code for genomeview.svg

import itertools
import numpy

class GraphicsBackend:
    pass
    
def _addOptions(kwdargs, defaults=None):
    if defaults is None: defaults = {}
    options = []
    defaults.update(kwdargs)
    for key, arg in defaults.items():
        if arg is not None and arg != "":
            options.append("""{key}="{arg}" """.format(key=key, arg=arg))
    return "".join(options)

class SVG(GraphicsBackend):
    _filter_id = 0

    def text(self, x, y, text, size=10, anchor="middle", family="Helvetica", **kwdargs):
        defaults = {}
        assert anchor in ["start", "middle", "end"]
        yield """<text x="{x:.2f}" y="{y:.2f}" font-size="{size}" font-family="{family}" text-anchor="{anchor}" {more}>{text}</text>""".format(
            x=x, y=y, size=size, family=family, anchor=anchor, more=_addOptions(kwdargs, defaults), text=text)

    def text_with_background(self, x, y, text, size=10, anchor="middle", text_color="black", bg="white", bg_opacity=0.8, **kwdargs):
        self._filter_id += 1

        text_filter = [
            """<defs>""",
            """    <filter x="0" y="0" width="1" height="1" id="solid{}">""".format(self._filter_id),
            """        <feFlood flood-opacity="{}" flood-color="{}"/>""".format(bg_opacity, bg),
            """        <feComposite in="SourceGraphic"/>""",
            """    </filter>""",
            """</defs>"""]
        for line in text_filter:
            yield line

        # this is a stoopid hack to get the filter to be fully behind the text, without making it blurry
        kwdargs["fill"] = bg
        kwdargs["filter"] = "url(#solid{})".format(self._filter_id)
        yield from self.text(x, y, text, size, anchor, **kwdargs)

        kwdargs["fill"] = text_color
        del kwdargs["filter"]
        yield from self.text(x, y, text, size, anchor, **kwdargs)



    def rect(self, x, y, width, height, **kwdargs):
        defaults = {"fill":"white", "stroke":"black"}
        tag = """<rect x="{x:.2f}" y="{y:.2f}" width="{w:.2f}" height="{h:.2f}" {more}/>""".format(
            x=x, y=y, w=width, h=height, more=_addOptions(kwdargs, defaults))
        yield tag

    def line(self, x1, y1, x2, y2, **kwdargs):
        defaults = {"stroke":"black"}
        yield """<line x1="{x1:.2f}" x2="{x2:.2f}" y1="{y1:.2f}" y2="{y2:.2f}" {more} />""".format(
            x1=x1, x2=x2,  y1=y1, y2=y2, more=_addOptions(kwdargs, defaults))

    def line_with_arrows(self, x1, y1, x2, y2, n=None, arrows=None, direction="right",
                         color="black", filled=True,
                         arrow_scale=None, arrowKwdArgs=None, **kwdargs):
        
        defaults = {"stroke":color}
        defaults.update(kwdargs)
        
        yield from self.line(x1, y1, x2, y2, **defaults)
        if arrowKwdArgs is None: arrowKwdArgs = {}

        if arrow_scale is None:
            arrow_scale = kwdargs.get("stroke-width", 1)

        if n is not None:
            arrows = numpy.arange(n) / n

        for arrow in arrows:
            x_arrow = x1+float(x2-x1)*arrow
            y_arrow = y1+float(y2-y1)*arrow
            yield from self.arrow(x_arrow, y_arrow, direction, filled=filled,
                color=color, scale=arrow_scale, **arrowKwdArgs)

    def arrow(self, x, y, direction, color="black", scale=1.0, filled=True, **kwdargs):
        more = _addOptions(kwdargs)

        if filled:
            fill = "fill=\"{color}\"".format(color=color)
            close = " z" # closes the path
        else:
            fill = "fill=\"transparent\""
            close = ""

        if direction == "right":
            path = """<path d="M {x0} {y0} L {x1} {y1} L {x2} {y2}{close}" stroke="{color}" """ \
                   """{fill} xcenter="{xcenter}" {more}/>"""
            a = path.format(
                x0=(x-2.5*scale), y0=(y-5*scale), 
                x1=(x+2.5*scale), y1=y, 
                x2=(x-2.5*scale), y2=(y+5*scale),
                close=close,
                fill=fill,
                color=color,
                xcenter=x,
                more=more)
        elif direction == "left":
            path = """<path d="M {x0} {y0} L {x1} {y1} L {x2} {y2}{close}" stroke="{color}" """ \
                   """{fill} xcenter="{xcenter}" {more}/>"""
            a = path.format(
                x0=(x+2.5*scale), y0=(y-5*scale), 
                x1=(x-2.5*scale), y1=y, 
                x2=(x+2.5*scale), y2=(y+5*scale),
                close=close,
                fill=fill,
                color=color,
                xcenter=x,
                more=more)
        yield a
        
    def block_arrow(self, left, top, width, height, arrow_width, direction, **kwdargs):
        coords = {"stroke": kwdargs.pop("stroke", "none"), "fill":kwdargs.pop("fill", "black")}
        coords["more"] = _addOptions(kwdargs)

        if direction == "right":
            path = """<path d="M {x0} {y0} L {x1} {y1} L {x2} {y2} L {x3} {y3} """ \
                   """L {x4} {y4} z" stroke="{stroke}" fill="{fill}" {more}/>"""
            coords["x0"], coords["y0"] = left, top,
            coords["x1"], coords["y1"] = left+width, top
            coords["x2"], coords["y2"] = left+width+arrow_width, top+height/2
            coords["x3"], coords["y3"] = left+width, top+height
            coords["x4"], coords["y4"] = left, top+height
        else:
            path = """<path d="M {x0} {y0} L {x1} {y1} L {x2} {y2} L {x3} {y3} """ \
                   """L {x4} {y4} z" stroke="{stroke}" fill="{fill}" {more}/>"""

            coords["x0"], coords["y0"] = left, top
            coords["x1"], coords["y1"] = left+width, top
            coords["x2"], coords["y2"] = left+width, top+height
            coords["x3"], coords["y3"] = left, top+height
            coords["x4"], coords["y4"] = left-arrow_width, top+height/2

        path = path.format(**coords)
        yield path


    def start_clipped_group(self, x, y, width, height, name):
        yield """<clipPath id="clip_path_{}"><rect x="{}" y="{}" width="{}" height="{}" /></clipPath>""".format(
            name, x, y, width, height)
        yield """<g clip-path="url(#clip_path_{})">""".format(name)
        
    def stop_clipped_group(self):
        yield "</g>"
    

[docs]class Renderer: newid = itertools.count() def __init__(self, backend, x, y, width, height): self.backend = backend self.x = x self.y = y self.width = width self.height = height self.id = next(Renderer.newid)
[docs] def text(self, x, y, *args, **kwdargs): yield from self.backend.text(x+self.x, y+self.y, *args, **kwdargs)
[docs] def text_with_background(self, x, y, *args, **kwdargs): yield from self.backend.text_with_background(x+self.x, y+self.y, *args, **kwdargs)
[docs] def rect(self, x, y, *args, **kwdargs): yield from self.backend.rect(x+self.x, y+self.y, *args, **kwdargs)
[docs] def line(self, x1, y1, x2, y2, *args, **kwdargs): yield from self.backend.line(x1+self.x, y1+self.y, x2+self.x, y2+self.y, *args, **kwdargs)
[docs] def line_with_arrows(self, x1, y1, x2, y2, *args, **kwdargs): yield from self.backend.line_with_arrows(x1+self.x, y1+self.y, x2+self.x, y2+self.y, *args, **kwdargs)
def arrow(self, x, y, *args, **kwdargs): yield from self.backend.arrow(x+self.x, y+self.y, *args, **kwdargs) def block_arrow(self, left, top, *args, **kwdargs): yield from self.backend.block_arrow(left+self.x, top+self.y, *args, **kwdargs) def render(self, element): yield "<!-- {} -->".format(element.name) yield from self.backend.start_clipped_group(self.x, self.y, self.width, self.height, self.id) # yield self.backend.rect(self.x, self.y, self.width, self.height, fill="blue") if hasattr(element, "prerenderers"): for prerenderer in element.prerenderers: for subelement in prerenderer(self, element): yield " " + subelement for subelement in element.render(self): yield " " + subelement if hasattr(element, "postrenderers"): for postrenderer in element.postrenderers: for subelement in postrenderer(self, element): yield " " + subelement yield from self.backend.stop_clipped_group() def subrenderer(self, x=0, y=0, width=None, height=None): if width is None: width = self.width if height is None: height = self.height assert width <= self.width assert height <= self.height, "{} {}".format(height, self.height) x += self.x y += self.y renderer = Renderer(self.backend, x, y, width, height) return renderer