Playing with SVG graphics in IPython

IPython's SVG display functionality, in conjunction with the ease of making SVG strings using ElementTrees, makes it really easy to have a nice drawing canvas inside of IPython.

In [2]:
from IPython.display import SVG

def tosvg(polygons,border=0):
    """
    Convert a list of polygons into an SVG image.
    Polygons are lists of x,y tuples.
    """
    import xml.etree.ElementTree as ET

    colors = ['aqua','blue','fuchsia','gray','green','lime','maroon',
              'navy','olive','purple','red','silver','teal','yellow']

    xmin,xmax,ymin,ymax = bbox(polygons,border)
    width = xmax-xmin
    height = ymax-ymin

    svg = ET.Element('svg', xmlns="http://www.w3.org/2000/svg", version="1.1",
                    height="%s" % height, width="%s" % width)
    for i,polygon in enumerate(polygons):
        point_list = " ".join(["%d,%d" % (x-xmin,y-ymin) for (x,y) in polygon])
        ET.SubElement(svg,"polygon",fill=colors[i%len(colors)],
                      stroke="black",points=point_list)
    #ET.dump(svg)
    return ET.tostring(svg)

def bbox(polygons,border=0,BIG=1e10):
    """
    Compute the bounding box of a list of polygons. Border adds an optional
    border amount to all values in the bbox. 
    """
    xmin=ymin = BIG
    xmax=ymax = -BIG
    for polygon in polygons:
        for x,y in polygon:
            xmax = max(xmax,x)
            xmin = min(xmin,x)
            ymax = max(ymax,y)
            ymin = min(ymin,y)
    return xmin-border,xmax+border,ymin-border,ymax+border

Given a list of polygons as a list of x,y tuples, you can display them:

In [3]:
polygons = [
    [(-20,0),(0,100),(5,100),(5,0)],
    [(1,1),(1,10),(10,10),(10,1)],
    [(10,10),(10,50),(50,50),(50,10)],
    [(50,50),(50,150),(150,150),(150,50)],
    ]
SVG(tosvg(polygons))
Out[3]:

and play with the padding:

In [4]:
SVG(tosvg(polygons,100))
Out[4]:

Fancier Version

Here's a fancier version I wrote a long time ago, updated to use _reprsvg for automatic display.

In [14]:
import xml.etree.ElementTree as ET

class SVGScene:
    def __init__(self):
        self.items = []
        self.height = 400 # override with bbox calculation
        self.width = 400 # override with bbox calculation
        return

    def add(self,item): self.items.append(item)
        
    def bbox(self,border=0,BIG=1e10):
        self.xmin = self.ymin = BIG
        self.xmax = self.ymax = -BIG
        for item in self.items:
            xmin,xmax,ymin,ymax = item.bbox()
            self.xmin = min(self.xmin,xmin)
            self.xmax = max(self.xmax,xmax)
            self.ymin = min(self.ymin,ymin)
            self.ymax = max(self.ymax,ymax)
        self.xmin -= border
        self.ymin -= border
        self.xmax += border
        self.ymax += border
        self.height = self.ymax
        self.width = self.xmax
        return
    
    def _repr_svg_(self): return self.to_svg()
    
    def to_svg(self):
        self.bbox(10)
        svg = ET.Element('svg', xmlns="http://www.w3.org/2000/svg", version="1.1",
                        height="%s" % self.height, width="%s" % self.width)
        g = ET.SubElement(svg,"g",style="fill-opacity:1.0; stroke:black; stroke-width:1;")
        for item in self.items:
            item.to_svg(g)
        #ET.dump(svg) # useful for debugging
        return ET.tostring(svg)

    def line(self,start,end): self.items.append(Line(start,end))
    def circle(self,center,radius,color='blue'): self.items.append(Circle(center,radius,color))
    def rectangle(self,origin,height,width,color='blue'): self.items.append(Rectangle(origin,height,width,color))
    def text(self,origin,text,size=24): self.items.append(Text(origin,text,size))
    
class Line:
    def __init__(self,start,end):
        self.start = start #xy tuple
        self.end = end     #xy tuple
        return
    
    def to_svg(self,parent):
        ET.SubElement(parent,"line",x1=str(self.start[0]),y1=str(self.start[1]),x2=str(self.end[0]),y2=str(self.end[1]))

    def bbox(self):
        return min(self.start[0],self.end[0]),max(self.start[0],self.end[0]),min(self.start[1],self.end[1]),max(self.start[1],self.end[1])


class Circle:
    def __init__(self,center,radius,color):
        self.center = center #xy tuple
        self.radius = radius #xy tuple
        self.color = color   #rgb tuple in range(0,256)
        return
    
    def to_svg(self,parent):
        color = colorstr(self.color)
        ET.SubElement(parent,"circle",cx=str(self.center[0]),cy=str(self.center[1]),r=str(self.radius),
                      style="fill:%s;" % color)

    def bbox(self):
        return self.center[0]-self.radius,self.center[0]+self.radius,self.center[1]-self.radius,self.center[1]+self.radius

class Rectangle:
    def __init__(self,origin,height,width,color):
        self.origin = origin
        self.height = height
        self.width = width
        self.color = color
        return

    def to_svg(self,parent):
        color = colorstr(self.color)
        ET.SubElement(parent,"rect",x=str(self.origin[0]),y=str(self.origin[1]),height=str(self.height),
                      width=str(self.width),style="fill:%s;" % color)

    def bbox(self):
        return self.origin[0],self.origin[0]+self.width,self.origin[1],self.origin[1]+self.height

class Text:
    def __init__(self,origin,text,size=24):
        self.origin = origin
        self.text = text
        self.size = size
        return

    def to_svg(self,parent):
        fs = "font-size"
        el = ET.SubElement(parent,"text",x=str(self.origin[0]),y=str(self.origin[1]))
        el.set("font-size",str(self.size))
        el.text = self.text
    
    def bbox(self):
        return self.origin[0],self.origin[0]+self.size,self.origin[1],self.origin[1]+self.size # Guessing here
    
def colorstr(rgb): 
    if type(rgb) == type(""): return rgb
    return "#%x%x%x" % (rgb[0]/16,rgb[1]/16,rgb[2]/16)

For example:

In [15]:
scene = SVGScene()
scene.rectangle((100,100),200,200,(0,255,255))
scene.line((200,200),(200,300))
scene.line((200,200),(300,200))
scene.line((200,200),(100,200))
scene.line((200,200),(200,100))
scene.circle((200,200),30,(0,0,255))
scene.circle((200,300),30,(0,255,0))
scene.circle((300,200),30,(255,0,0))
scene.circle((100,200),30,(255,255,0))
scene.circle((200,100),30,"fuchsia")
scene.text((50,50),"Testing SVG")
scene
Out[15]:
Testing SVG