Skip to content

Instantly share code, notes, and snippets.

@rpmuller
Last active January 23, 2019 05:53
Show Gist options
  • Star 4 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save rpmuller/5666810 to your computer and use it in GitHub Desktop.
Save rpmuller/5666810 to your computer and use it in GitHub Desktop.
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.
Display the source blob
Display the rendered blob
Raw
{
"metadata": {
"name": "svg-display"
},
"nbformat": 3,
"nbformat_minor": 0,
"worksheets": [
{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": "# Playing with SVG graphics in IPython"
},
{
"cell_type": "markdown",
"metadata": {},
"source": "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."
},
{
"cell_type": "code",
"collapsed": false,
"input": "from IPython.display import SVG\n\ndef tosvg(polygons,border=0):\n \"\"\"\n Convert a list of polygons into an SVG image.\n Polygons are lists of x,y tuples.\n \"\"\"\n import xml.etree.ElementTree as ET\n\n colors = ['aqua','blue','fuchsia','gray','green','lime','maroon',\n 'navy','olive','purple','red','silver','teal','yellow']\n\n xmin,xmax,ymin,ymax = bbox(polygons,border)\n width = xmax-xmin\n height = ymax-ymin\n\n svg = ET.Element('svg', xmlns=\"http://www.w3.org/2000/svg\", version=\"1.1\",\n height=\"%s\" % height, width=\"%s\" % width)\n for i,polygon in enumerate(polygons):\n point_list = \" \".join([\"%d,%d\" % (x-xmin,y-ymin) for (x,y) in polygon])\n ET.SubElement(svg,\"polygon\",fill=colors[i%len(colors)],\n stroke=\"black\",points=point_list)\n #ET.dump(svg)\n return ET.tostring(svg)\n\ndef bbox(polygons,border=0,BIG=1e10):\n \"\"\"\n Compute the bounding box of a list of polygons. Border adds an optional\n border amount to all values in the bbox. \n \"\"\"\n xmin=ymin = BIG\n xmax=ymax = -BIG\n for polygon in polygons:\n for x,y in polygon:\n xmax = max(xmax,x)\n xmin = min(xmin,x)\n ymax = max(ymax,y)\n ymin = min(ymin,y)\n return xmin-border,xmax+border,ymin-border,ymax+border",
"language": "python",
"metadata": {},
"outputs": [],
"prompt_number": 2
},
{
"cell_type": "markdown",
"metadata": {},
"source": "Given a list of polygons as a list of x,y tuples, you can display them:"
},
{
"cell_type": "code",
"collapsed": false,
"input": "polygons = [\n [(-20,0),(0,100),(5,100),(5,0)],\n [(1,1),(1,10),(10,10),(10,1)],\n [(10,10),(10,50),(50,50),(50,10)],\n [(50,50),(50,150),(150,150),(150,50)],\n ]\nSVG(tosvg(polygons))",
"language": "python",
"metadata": {},
"outputs": [
{
"output_type": "pyout",
"prompt_number": 3,
"svg": "<svg height=\"150\" version=\"1.1\" width=\"170\" xmlns=\"http://www.w3.org/2000/svg\"><polygon fill=\"aqua\" points=\"0,0 20,100 25,100 25,0\" stroke=\"black\"/><polygon fill=\"blue\" points=\"21,1 21,10 30,10 30,1\" stroke=\"black\"/><polygon fill=\"fuchsia\" points=\"30,10 30,50 70,50 70,10\" stroke=\"black\"/><polygon fill=\"gray\" points=\"70,50 70,150 170,150 170,50\" stroke=\"black\"/></svg>",
"text": "<IPython.core.display.SVG at 0x112e66e50>"
}
],
"prompt_number": 3
},
{
"cell_type": "markdown",
"metadata": {},
"source": "and play with the padding:"
},
{
"cell_type": "code",
"collapsed": false,
"input": "SVG(tosvg(polygons,100))",
"language": "python",
"metadata": {},
"outputs": [
{
"output_type": "pyout",
"prompt_number": 4,
"svg": "<svg height=\"350\" version=\"1.1\" width=\"370\" xmlns=\"http://www.w3.org/2000/svg\"><polygon fill=\"aqua\" points=\"100,100 120,200 125,200 125,100\" stroke=\"black\"/><polygon fill=\"blue\" points=\"121,101 121,110 130,110 130,101\" stroke=\"black\"/><polygon fill=\"fuchsia\" points=\"130,110 130,150 170,150 170,110\" stroke=\"black\"/><polygon fill=\"gray\" points=\"170,150 170,250 270,250 270,150\" stroke=\"black\"/></svg>",
"text": "<IPython.core.display.SVG at 0x112e59c50>"
}
],
"prompt_number": 4
},
{
"cell_type": "markdown",
"metadata": {},
"source": "## Fancier Version\nHere's a fancier version I wrote a long time ago, updated to use _repr_svg_ for automatic display."
},
{
"cell_type": "code",
"collapsed": false,
"input": "import xml.etree.ElementTree as ET\n\nclass SVGScene:\n def __init__(self):\n self.items = []\n self.height = 400 # override with bbox calculation\n self.width = 400 # override with bbox calculation\n return\n\n def add(self,item): self.items.append(item)\n \n def bbox(self,border=0,BIG=1e10):\n self.xmin = self.ymin = BIG\n self.xmax = self.ymax = -BIG\n for item in self.items:\n xmin,xmax,ymin,ymax = item.bbox()\n self.xmin = min(self.xmin,xmin)\n self.xmax = max(self.xmax,xmax)\n self.ymin = min(self.ymin,ymin)\n self.ymax = max(self.ymax,ymax)\n self.xmin -= border\n self.ymin -= border\n self.xmax += border\n self.ymax += border\n self.height = self.ymax\n self.width = self.xmax\n return\n \n def _repr_svg_(self): return self.to_svg()\n \n def to_svg(self):\n self.bbox(10)\n svg = ET.Element('svg', xmlns=\"http://www.w3.org/2000/svg\", version=\"1.1\",\n height=\"%s\" % self.height, width=\"%s\" % self.width)\n g = ET.SubElement(svg,\"g\",style=\"fill-opacity:1.0; stroke:black; stroke-width:1;\")\n for item in self.items:\n item.to_svg(g)\n #ET.dump(svg) # useful for debugging\n return ET.tostring(svg)\n\n def line(self,start,end): self.items.append(Line(start,end))\n def circle(self,center,radius,color='blue'): self.items.append(Circle(center,radius,color))\n def rectangle(self,origin,height,width,color='blue'): self.items.append(Rectangle(origin,height,width,color))\n def text(self,origin,text,size=24): self.items.append(Text(origin,text,size))\n \nclass Line:\n def __init__(self,start,end):\n self.start = start #xy tuple\n self.end = end #xy tuple\n return\n \n def to_svg(self,parent):\n ET.SubElement(parent,\"line\",x1=str(self.start[0]),y1=str(self.start[1]),x2=str(self.end[0]),y2=str(self.end[1]))\n\n def bbox(self):\n 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])\n\n\nclass Circle:\n def __init__(self,center,radius,color):\n self.center = center #xy tuple\n self.radius = radius #xy tuple\n self.color = color #rgb tuple in range(0,256)\n return\n \n def to_svg(self,parent):\n color = colorstr(self.color)\n ET.SubElement(parent,\"circle\",cx=str(self.center[0]),cy=str(self.center[1]),r=str(self.radius),\n style=\"fill:%s;\" % color)\n\n def bbox(self):\n return self.center[0]-self.radius,self.center[0]+self.radius,self.center[1]-self.radius,self.center[1]+self.radius\n\nclass Rectangle:\n def __init__(self,origin,height,width,color):\n self.origin = origin\n self.height = height\n self.width = width\n self.color = color\n return\n\n def to_svg(self,parent):\n color = colorstr(self.color)\n ET.SubElement(parent,\"rect\",x=str(self.origin[0]),y=str(self.origin[1]),height=str(self.height),\n width=str(self.width),style=\"fill:%s;\" % color)\n\n def bbox(self):\n return self.origin[0],self.origin[0]+self.width,self.origin[1],self.origin[1]+self.height\n\nclass Text:\n def __init__(self,origin,text,size=24):\n self.origin = origin\n self.text = text\n self.size = size\n return\n\n def to_svg(self,parent):\n fs = \"font-size\"\n el = ET.SubElement(parent,\"text\",x=str(self.origin[0]),y=str(self.origin[1]))\n el.set(\"font-size\",str(self.size))\n el.text = self.text\n \n def bbox(self):\n return self.origin[0],self.origin[0]+self.size,self.origin[1],self.origin[1]+self.size # Guessing here\n \ndef colorstr(rgb): \n if type(rgb) == type(\"\"): return rgb\n return \"#%x%x%x\" % (rgb[0]/16,rgb[1]/16,rgb[2]/16)",
"language": "python",
"metadata": {},
"outputs": [],
"prompt_number": 14
},
{
"cell_type": "markdown",
"metadata": {},
"source": "For example:"
},
{
"cell_type": "code",
"collapsed": false,
"input": "scene = SVGScene()\nscene.rectangle((100,100),200,200,(0,255,255))\nscene.line((200,200),(200,300))\nscene.line((200,200),(300,200))\nscene.line((200,200),(100,200))\nscene.line((200,200),(200,100))\nscene.circle((200,200),30,(0,0,255))\nscene.circle((200,300),30,(0,255,0))\nscene.circle((300,200),30,(255,0,0))\nscene.circle((100,200),30,(255,255,0))\nscene.circle((200,100),30,\"fuchsia\")\nscene.text((50,50),\"Testing SVG\")\nscene",
"language": "python",
"metadata": {},
"outputs": [
{
"output_type": "pyout",
"prompt_number": 15,
"svg": "<svg height=\"340\" version=\"1.1\" width=\"340\" xmlns=\"http://www.w3.org/2000/svg\"><g style=\"fill-opacity:1.0; stroke:black; stroke-width:1;\"><rect height=\"200\" style=\"fill:#0ff;\" width=\"200\" x=\"100\" y=\"100\" /><line x1=\"200\" x2=\"200\" y1=\"200\" y2=\"300\" /><line x1=\"200\" x2=\"300\" y1=\"200\" y2=\"200\" /><line x1=\"200\" x2=\"100\" y1=\"200\" y2=\"200\" /><line x1=\"200\" x2=\"200\" y1=\"200\" y2=\"100\" /><circle cx=\"200\" cy=\"200\" r=\"30\" style=\"fill:#00f;\" /><circle cx=\"200\" cy=\"300\" r=\"30\" style=\"fill:#0f0;\" /><circle cx=\"300\" cy=\"200\" r=\"30\" style=\"fill:#f00;\" /><circle cx=\"100\" cy=\"200\" r=\"30\" style=\"fill:#ff0;\" /><circle cx=\"200\" cy=\"100\" r=\"30\" style=\"fill:fuchsia;\" /><text font-size=\"24\" x=\"50\" y=\"50\">Testing SVG</text></g></svg>",
"text": "<__main__.SVGScene instance at 0x1129e6c20>"
}
],
"prompt_number": 15
},
{
"cell_type": "code",
"collapsed": false,
"input": "",
"language": "python",
"metadata": {},
"outputs": [],
"prompt_number": 6
}
],
"metadata": {}
}
]
}
@Carreau
Copy link

Carreau commented Jun 5, 2013

may I suggest adding a _repr_svg_ to Scene ?

@rpmuller
Copy link
Author

@Carreau - great idea. Done!

@ohlr
Copy link

ohlr commented Nov 20, 2018

To get it working with Jupyter + Python 3.6 I had to do the following:
return "#%x%x%x" % (int(rgb[0]/16),int(rgb[1]/16),int(rgb[2]/16))
SVG(scene.to_svg())

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment