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.
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:
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))
and play with the padding:
SVG(tosvg(polygons,100))
Here's a fancier version I wrote a long time ago, updated to use repr_svg for automatic display.
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:
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