Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Merge branch 'master' of https://github.com/mynameisfiber/d3py

  • Loading branch information...
commit 9f9b4d1ba006be3091904254a2efcd310fc44147 2 parents cfb03a2 + 7416be4
Mike Dewar authored
95 d3py/HTTPHandler.py
View
@@ -1,38 +1,71 @@
import SimpleHTTPServer
-
-import os
-import posixpath
-import urllib
import SocketServer
+from cStringIO import StringIO
+import sys
class ThreadedHTTPServer(SocketServer.ThreadingMixIn, SocketServer.TCPServer):
allow_reuse_address = True
+
class CustomHTTPRequestHandler(SimpleHTTPServer.SimpleHTTPRequestHandler):
- def translate_path(self, path):
- """Translate a /-separated PATH to the local filename syntax.
-
- Components that mean special things to the local file system
- (e.g. drive or directory names) are ignored. (XXX They should
- probably be diagnosed.)
-
- """
- # make sure a working directory has been set
- try:
- if not os.path.isdir(self.directory):
- self.directory = os.getcwd()
- except:
- self.directory = os.getcwd()
- # abandon query parameters
- path = path.split('?',1)[0]
- path = path.split('#',1)[0]
- path = posixpath.normpath(urllib.unquote(path))
- words = path.split('/')
- words = filter(None, words)
- path = self.directory
- for word in words:
- drive, word = os.path.splitdrive(word)
- head, word = os.path.split(word)
- if word in (os.curdir, os.pardir): continue
- path = os.path.join(path, word)
- return path
+ def log_message(self, format, *args):
+ """
+ We get rid of the BaseHTTPRequestHandler logging messages
+ because they can get annoying!
+ """
+ if self.logging:
+ super().log_message(format, *args)
+
+ def do_GET(self):
+ """Serve a GET request."""
+ f = self.send_head()
+ if f:
+ self.copyfile(f, self.wfile)
+ f.seek(0)
+
+ def do_HEAD(self):
+ """Serve a HEAD request."""
+ f = self.send_head()
+ if f:
+ f.seek(0)
+
+ def send_head(self):
+ """
+ This sends the response code and MIME headers.
+
+ Return value is either a file object (which has to be copied
+ to the outputfile by the caller unless the command was HEAD,
+ and must be closed by the caller under all circumstances), or
+ None, in which case the caller has nothing further to do.
+
+ """
+ path = self.path[1:] #get rid of leading '/'
+ f = None
+ ctype = self.guess_type(path)
+ try:
+ f = self.filemap[path]["fd"]
+ except KeyError:
+ return self.list_directory()
+ self.send_response(200)
+ self.send_header("Content-type", ctype)
+ self.send_header("Last-Modified", self.date_time_string(self.filemap[path]["timestamp"]))
+ self.end_headers()
+ return f
+
+ def list_directory(self):
+ f = StringIO()
+ f.write('<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">')
+ f.write("<html>\n<title>Directory listing</title>\n")
+ f.write("<body>\n<h2>Directory listing</h2>\n")
+ f.write("<hr>\n<ul>\n")
+ for path, meta in self.filemap.iteritems():
+ f.write('<li><a href="%s">%s</a>\n' % (path, path))
+ f.write("</ul>\n<hr>\n</body>\n</html>\n")
+ length = f.tell()
+ f.seek(0)
+ self.send_response(200)
+ encoding = sys.getfilesystemencoding()
+ self.send_header("Content-type", "text/html; charset=%s" % encoding)
+ self.send_header("Content-Length", str(length))
+ self.end_headers()
+ return f
124 d3py/d3py.py
View
@@ -1,6 +1,5 @@
from css import CSS
import javascript as JS
-import templates
import numpy as np
import logging
@@ -9,15 +8,14 @@
from HTTPHandler import CustomHTTPRequestHandler, ThreadedHTTPServer
import threading
-import os
-import tempfile
-import shutil
+from cStringIO import StringIO
+import time
+
import json
class D3object(object):
-
def build_js():
raise NotImplementedError
@@ -30,16 +28,16 @@ def build_html():
def build_geoms():
raise NotImplementedError
- def save_data(self, where=None):
+ def save_data(self):
raise NotImplementedError
- def save_css(self, where=None):
+ def save_css(self):
raise NotImplementedError
- def save_js(self, where=None):
+ def save_js(self):
raise NotImplementedError
- def save_html(self, where=None):
+ def save_html(self):
raise NotImplementedError
def build(self):
@@ -48,20 +46,15 @@ def build(self):
self.build_html()
self.build_geoms()
- def update(self, where=None):
+ def update(self):
self.build()
- self.save(where)
+ self.save()
- def save(self, where=None):
- if where is not None and not os.path.isdir(where):
- try:
- os.makedirs(where)
- except Exception, e:
- print "Could not create directory structure %s: %s"%(where, e)
- self.save_data(where)
- self.save_css(where)
- self.save_js(where)
- self.save_html(where)
+ def save(self):
+ self.save_data()
+ self.save_css()
+ self.save_js()
+ self.save_html()
def clanup(self):
raise NotImplementedError
@@ -83,9 +76,8 @@ def __del__(self):
class Figure(D3object):
-
- def __init__(self, data, name="figure",
- width=400, height=100, port=8000, template=None, font="Asap", **kwargs):
+ def __init__(self, data, name="figure", width=400, height=100, port=8000,
+ template=None, font="Asap", logging=False, **kwargs):
"""
data : dataFrame
data used for the plot. This dataFrame is column centric
@@ -101,25 +93,25 @@ def __init__(self, data, name="figure",
# store data
self.name = name
self.data = data
- self.work_dir = tempfile.mkdtemp(prefix="d3py-%s"%self.name)
+ self.filemap = {"static/d3.js":{"fd":open("static/d3.js","r"), "timestamp":time.time()},}
self.save_data()
+
# Networking stuff
self.port = port
self.server_thread = None
self.httpd = None
self.interactive = False
+ self.logging = logging
+
# initialise strings
- self.draw = JS.Function("draw", ["data"], "")
+ self.js = JS.JavaScript()
+ self.margins = {"top": 10, "right": 20, "bottom": 25, "left": 60, "height":height, "width":width}
# we use bostock's scheme http://bl.ocks.org/1624660
- self.margins = {"top": 10, "right": 20, "bottom": 25, "left": 60}
- self.draw += "var margin = %s;"%json.dumps(self.margins).replace('""','')
- self.draw += " width = %s - margin.left - margin.right"%width
- self.draw += " height = %s - margin.top - margin.bottom;"%height
self.css = CSS()
self.html = ""
- self.template = template or 'static/d3py_template.html'
- self.js_geoms = ""
+ self.template = template or "".join(open('static/d3py_template.html').readlines())
+ self.js_geoms = JS.JavaScript()
self.css_geoms = CSS()
self.geoms = []
# misc arguments - these go into the css!
@@ -205,22 +197,21 @@ def build_scales(self):
def build_js(self):
- draw_code = JS.JavaScript()
-
+ draw = JS.Function("draw", ("data",))
+ draw += "var margin = %s;"%json.dumps(self.margins).replace('""','')
+ draw += " width = %s - margin.left - margin.right"%self.margins["width"]
+ draw += " height = %s - margin.top - margin.bottom;"%self.margins["height"]
# this approach to laying out the graph is from Bostock: http://bl.ocks.org/1624660
-
- draw_code += "var g = " + JS.Object("d3").select("'#chart'") \
+ draw += "var g = " + JS.Object("d3").select("'#chart'") \
.append("'svg'") \
- .attr("'width'", 'width + margin.left + margin.right') \
- .attr("'height'", 'height + margin.top + margin.bottom') \
+ .attr("'width'", 'width + margin.left + margin.right + 25') \
+ .attr("'height'", 'height + margin.top + margin.bottom + 25') \
.append("'g'") \
.attr("'transform'", "'translate(' + margin.left + ',' + margin.top + ')'")
scale = self.build_scales()
-
- draw_code += "var scales = %s;"%json.dumps(scale, sort_keys=True, indent=4).replace('"', '')
-
- self.draw += draw_code
+ draw += "var scales = %s;"%json.dumps(scale, sort_keys=True, indent=4).replace('"', '')
+ self.js = JS.JavaScript() + draw + JS.Function("init")
def build_css(self):
# build up the basic css
@@ -231,7 +222,7 @@ def build_css(self):
def build_html(self):
# we start the html using a template - it's pretty simple
- self.html = templates.d3py_template
+ self.html = self.template
self.html = self.html.replace("{{ name }}", self.name)
self.html = self.html.replace("{{ font }}", self.font)
self.save_html()
@@ -240,7 +231,7 @@ def build_geoms(self):
self.js_geoms = JS.JavaScript()
self.css_geoms = CSS()
for geom in self.geoms:
- self.js_geoms += geom.build_js()
+ self.js_geoms.merge(geom.build_js())
self.css_geoms += geom.build_css()
def __add__(self, geom):
@@ -284,30 +275,35 @@ def save_data(self,directory=None):
specify a directory to store the data in (optional)
"""
# write data
- fname = "%s/%s.json"%(directory or self.work_dir, self.name)
- fh = open(fname, 'w+')
- fh.write(self.data_to_json())
- fh.close()
+ filename = "%s.json"%self.name
+ self.filemap[filename] = {"fd":StringIO(self.data_to_json()),
+ "timestamp":time.time()}
- def save_css(self, where=None):
+ def save_css(self):
# write css
- fh = open("%s/%s.css"%(where or self.work_dir, self.name), 'w+')
- fh.write("%s\n%s"%(self.css, self.css_geoms))
- fh.close()
+ filename = "%s.css"%self.name
+ css = "%s\n%s"%(self.css, self.css_geoms)
+ self.filemap[filename] = {"fd":StringIO(css),
+ "timestamp":time.time()}
- def save_js(self, where=None):
+ def save_js(self):
# write javascript
- fh = open("%s/%s.js"%(where or self.work_dir, self.name), 'w+')
- fh.write("%s"%(self.draw + self.js_geoms))
- fh.close()
+ final_js = JS.JavaScript()
+ final_js.merge(self.js)
+ final_js.merge(self.js_geoms)
- def save_html(self, where=None):
+ filename = "%s.js"%self.name
+ js = "%s"%final_js
+ self.filemap[filename] = {"fd":StringIO(js),
+ "timestamp":time.time()}
+
+ def save_html(self):
# update the html with the correct port number
self.html = self.html.replace("{{ port }}", str(self.port))
# write html
- fh = open("%s/%s.html"%(where or self.work_dir,self.name),'w+')
- fh.write(self.html)
- fh.close()
+ filename = "%s.html"%self.name
+ self.filemap[filename] = {"fd":StringIO(self.html),
+ "timestamp":time.time()}
def show(self, interactive=None):
super(Figure, self).show()
@@ -331,7 +327,8 @@ def serve(self, blocking=True):
"""
if self.server_thread is None or self.server_thread.active_count() == 0:
Handler = CustomHTTPRequestHandler
- Handler.directory = self.work_dir
+ Handler.filemap = self.filemap
+ Handler.logging = self.logging
try:
self.httpd = ThreadedHTTPServer(("", self.port), Handler)
except Exception, e:
@@ -350,11 +347,6 @@ def serve(self, blocking=True):
def cleanup(self):
try:
- try:
- print "Cleaning temp files"
- shutil.rmtree(self.work_dir)
- except:
- pass
if self.httpd is not None:
print "Shutting down httpd"
self.httpd.shutdown()
2  d3py/geoms/__init__.py
View
@@ -1,6 +1,8 @@
#!/usr/local/bin/python
from xaxis import xAxis
+from yaxis import yAxis
+from axis import Axis
from point import Point
from bar import Bar
from line import Line
44 d3py/geoms/axis.py
View
@@ -0,0 +1,44 @@
+from geom import Geom, JavaScript, Object, Function
+
+class Axis(Geom):
+ def __init__(self, var, orient=None, **kwargs):
+ """
+ var : string
+ name of the column you want to use to define the axis
+ """
+ Geom.__init__(self, **kwargs)
+ self.var = var
+ self.orient = orient
+ self.params = [var]
+ self._id = '%s_axis'%var
+ self.name = '%s_axis'%var
+ self.build_css()
+ self.build_js()
+
+ def build_js(self):
+ draw = Function("draw", ("data",), [])
+ scale = "scales.%s_%s"%(self.var, self.var)
+ draw += "%s = d3.svg.axis().scale(%s)%s"%(self.name, scale, ".orient('%s')"%self.orient if self.orient else "")
+
+ axis_group = Object("g").append('"g"') \
+ .attr('"class"','"%s"'%self.name) \
+ .attr('"transform"', '"translate(" + margin + ",0)"') \
+ .call(self.name)
+ draw += axis_group
+
+ self.js = JavaScript() + draw
+ return self.js
+
+ def build_css(self):
+ axis_path = {
+ "fill" : "none",
+ "stroke" : "#000"
+ }
+ self.css[".%s path"%self.name] = axis_path
+ axis_path = {
+ "fill" : "none",
+ "stroke" : "#000"
+ }
+ self.css[".%s line"%self.name] = axis_path
+
+ return self.css
8 d3py/geoms/bar.py
View
@@ -36,8 +36,8 @@ def build_js(self):
"return height - scales.%(y)s_y(d.%(y)s)"%{"y":self.y}
)
- self.js = JavaScript()
- self.js += Object("g").selectAll("'.bars'") \
+ draw = Function("draw", ("data",), [])
+ draw += Object("g").selectAll("'.bars'") \
.data("data") \
.enter() \
.append("'rect'") \
@@ -47,6 +47,10 @@ def build_js(self):
.attr("'y'", yfxn) \
.attr("'width'", "scales.%s_x.rangeBand()"%self.x)\
.attr("'height'", heightfxn)
+ # TODO: rangeBand above breaks for histogram type bar-plots... fix!
+
+ self.js = JavaScript() + draw
+ self.js += (Function("init", autocall=True) + "console.debug('Hi');")
return self.js
def build_css(self):
9 d3py/geoms/line.py
View
@@ -13,18 +13,21 @@ def __init__(self,x,y,**kwargs):
def build_js(self):
# add the line
+ draw = Function("draw", ("data", ))
+
x_fxn = Function(None, "d", "return scales.%s_x(d.%s)"%(self.x, self.x))
y_fxn = Function(None, "d", "return scales.%s_y(d.%s)"%(self.y, self.y))
- self.js = JavaScript()
- self.js += "var line = " + Object("d3.svg").add_attribute("line") \
+ draw += "var line = " + Object("d3.svg").add_attribute("line") \
.add_attribute("x", x_fxn) \
.add_attribute("y", y_fxn)
- self.js += Object("g").append("'svg:path'") \
+ draw += Object("g").append("'svg:path'") \
.attr("'d'", "line(data)") \
.attr("'class'", "'geom_line'") \
.attr("'id'", "'line_%s_%s'"%(self.x, self.y))
+
+ self.js = JavaScript(draw)
return self.js
def build_css(self):
5 d3py/geoms/point.py
View
@@ -26,6 +26,7 @@ def build_css(self):
return self.css
def build_js(self):
+ draw = Function("draw", ("data",))
js_cx = Function(None, "d", "return scales.%s_x(d.%s);"%(self.x,self.x))
js_cy = Function(None, "d", "return scales.%s_y(d.%s);"%(self.y,self.y))
@@ -41,5 +42,7 @@ def build_js(self):
if self.c:
fill = Function(None, "return d.%s;"%self.c)
obj.add_attribute("style", "fill", fill)
- self.js = JavaScript(obj)
+
+ draw += obj
+ self.js = JavaScript(draw)
return self.js
22 d3py/geoms/xaxis.py
View
@@ -1,13 +1,14 @@
-from geom import Geom, JavaScript, Object
+from geom import Geom, JavaScript, Object, Function
class xAxis(Geom):
- def __init__(self,x, **kwargs):
+ def __init__(self,x, label=None, **kwargs):
"""
x : string
name of the column you want to use to define the x-axis
"""
Geom.__init__(self, **kwargs)
self.x = x
+ self.label = label
self.params = [x]
self._id = 'xaxis'
self.name = 'xaxis'
@@ -15,15 +16,26 @@ def __init__(self,x, **kwargs):
self.build_js()
def build_js(self):
+ draw = Function("draw", ("data",), [])
scale = "scales.%s_x"%self.x
- self.js = JavaScript()
- self.js += "xAxis = d3.svg.axis().scale(%s)"%scale
+ draw += "xAxis = d3.svg.axis().scale(%s)"%scale
xaxis_group = Object("g").append('"g"') \
.attr('"class"','"xaxis"') \
.attr('"transform"', '"translate(0," + height + ")"') \
.call("xAxis")
- self.js += xaxis_group
+ draw += xaxis_group
+
+ if self.label:
+ # TODO: Have the transform on this label be less hacky
+ label_group = Object("g").append('"text"') \
+ .add_attribute("text", '"%s"'%self.label) \
+ .attr('"text-anchor"', '"middle"') \
+ .attr('"x"', "width/2") \
+ .attr('"y"', "height+45")
+ draw += label_group
+
+ self.js = JavaScript() + draw
return self.js
def build_css(self):
53 d3py/geoms/yaxis.py
View
@@ -0,0 +1,53 @@
+from geom import Geom, JavaScript, Object, Function
+
+class yAxis(Geom):
+ def __init__(self,y, label=None, **kwargs):
+ """
+ y : string
+ name of the column you want to use to define the y-axis
+ """
+ Geom.__init__(self, **kwargs)
+ self.y = y
+ self.label = label
+ self.params = [y]
+ self._id = 'yaxis'
+ self.name = 'yaxis'
+ self.build_css()
+ self.build_js()
+
+ def build_js(self):
+ draw = Function("draw", ("data",), [])
+ scale = "scales.%s_y"%self.y
+ draw += "yAxis = d3.svg.axis().scale(%s).orient('left')"%scale
+
+ yaxis_group = Object("g").append('"g"') \
+ .attr('"class"','"yaxis"') \
+ .call("yAxis")
+ draw += yaxis_group
+
+ if self.label:
+ # TODO: Have the transform on this label be less hacky
+ label_group = Object("g").append('"text"') \
+ .add_attribute("text", '"%s"'%self.label) \
+ .attr('"y"', '- margin.left + 15') \
+ .attr('"x"', '- height / 2.0') \
+ .attr('"text-anchor"', '"middle"') \
+ .attr('"transform"', '"rotate(-90, 0, 0)"')
+ draw += label_group
+
+ self.js = JavaScript() + draw
+ return self.js
+
+ def build_css(self):
+ axis_path = {
+ "fill" : "none",
+ "stroke" : "#000"
+ }
+ self.css[".yaxis path"] = axis_path
+ axis_path = {
+ "fill" : "none",
+ "stroke" : "#000"
+ }
+ self.css[".yaxis line"] = axis_path
+
+ return self.css
30 d3py/javascript.py
View
@@ -14,8 +14,17 @@ def __init__(self, statements=None):
else:
raise Exception("Invalid inputed statement type")
+ def merge(self, other):
+ for line in other.statements:
+ if hasattr(line, "name") and (line.name, type(line.__class__)) in self.objects_lookup:
+ idx = self.objects_lookup[(line.name, type(line.__class__))][1]
+ self.statements[idx] += line
+ else:
+ self.statements.append(line)
+ self.objects_lookup = self.parse_objects()
+
def get_object(self, name, objtype):
- return self.objects_lookup[(name,type(objtype))]
+ return self.objects_lookup[(name,type(objtype))][0]
def __getitem__(self, item):
return self.statements[item]
@@ -25,10 +34,10 @@ def __setitem__(self, item, value):
def parse_objects(self):
objects = {}
- for item in self.statements:
+ for i, item in enumerate(self.statements):
if hasattr(item, "name") and item.name:
# Is it necissary to compound the key with the class type?
- objects[ (item.name, type(item.__class__)) ] = item
+ objects[ (item.name, type(item.__class__)) ] = (item, i)
return objects
def _obj_to_statements(self, other):
@@ -118,7 +127,7 @@ def __str__(self):
return obj
class Function:
- def __init__(self, name, arguments, statements):
+ def __init__(self, name=None, arguments=None, statements=None, autocall=False):
"""
name: string
@@ -139,15 +148,18 @@ def __init__(self, name, arguments, statements):
self.arguments = arguments
if isinstance(statements, str):
statements = [statements, ]
- self.statements = statements
+ self.statements = statements or []
+ self.autocall = autocall
def _obj_to_statements(self, other):
if isinstance(other, str):
other = [other, ]
elif isinstance(other, JavaScript):
other = other.statements
- elif isinstance(other, Function) and other.name == self.name and other.arugments == self.arguments:
+ elif isinstance(other, Function) and other.name == self.name and other.arguments == self.arguments:
other = other.statements
+ elif isinstance(other, Object):
+ other = [other, ]
else:
other = None
return other
@@ -155,13 +167,13 @@ def _obj_to_statements(self, other):
def __add__(self, more_statements):
more_statements = self._obj_to_statements(more_statements)
if isinstance(more_statements, (list, tuple)):
- return Function(self.name, self.arguments, self.statements + more_statements)
+ return Function(self.name, self.arguments, self.statements + more_statements, self.autocall)
raise NotImplementedError
def __radd__(self, more_statements):
more_statements = self._obj_to_statements(more_statements)
if isinstance(more_statements, (list, tuple)):
- return Function(self.name, self.arguments, more_statements + self.statements)
+ return Function(self.name, self.arguments, more_statements + self.statements, self.autocall)
raise NotImplementedError
def __repr__(self):
@@ -175,4 +187,6 @@ def __str__(self):
for line in self.statements:
fxn += "\t%s\n"%str(line)
fxn += "}\n"
+ if self.autocall:
+ fxn += "%s(%s);\n"%(self.name, ",".join(self.arguments or ""))
return fxn
17 examples/d3py_hist.py
View
@@ -0,0 +1,17 @@
+import pandas
+import d3py
+import numpy as np
+
+x = np.random.gamma(1, size=100)
+count, bins = np.histogram(x)
+
+df = pandas.DataFrame({
+ "count" : count,
+ "apple_type" : bins[:-1]
+})
+
+with d3py.Figure(df) as p:
+ p += d3py.Bar(x="apple_type", y = "count", fill = "MediumAquamarine")
+ p += d3py.xAxis(x="apple_type", label="Apple Type")
+ p += d3py.yAxis(y="count", label="Number")
+ p.show()
1  examples/d3py_line.py
View
@@ -15,4 +15,5 @@
fig += d3py.geoms.Line('x', 'y')
fig += d3py.geoms.Point('x', 'y', fill='BlueViolet')
fig += d3py.xAxis('x')
+ fig += d3py.yAxis('y')
fig.show()
3  examples/d3py_scatter.py
View
@@ -10,5 +10,6 @@
with d3py.Figure(df, "my_figure", width=400, height=400) as fig:
fig += d3py.Point("d1", "d2", fill="DodgerBlue")
- fig += d3py.xAxis('d1')
+ fig += d3py.xAxis('d1', label="Random")
+ fig += d3py.yAxis('d2', label="Also random")
fig.show()
2  setup.py
View
@@ -8,5 +8,5 @@
author='Mike Dewar, Micha Gorelick and Adam Laiacano',
author_email='md@bit.ly',
url='https://github.com/mikedewar/D3py',
- packages=['d3py', 'd3py.geoms']
+ packages=['d3py', 'd3py.geoms', ]
)
9,342 static/d3.js
View
9,342 additions, 0 deletions not shown
21 static/d3py_template.html
View
@@ -1,16 +1,19 @@
-<html>
<head>
- <script type="text/javascript" src="http://mbostock.github.com/d3/d3.js"></script>
- <script type="text/javascript" src="http://localhost:{{ port }}/{{ name }}.js"></script>
- <link type="text/css" rel="stylesheet" href="http://localhost:{{ port }}/{{ name }}.css">
- <title>d3py: {{ name }}</title>
+ <META HTTP-EQUIV="CACHE-CONTROL" CONTENT="NO-CACHE">
+ <script type="text/javascript" src="http://localhost:{{ port }}/static/d3.js"></script>
+ <script type="text/javascript" src="http://localhost:{{ port }}/{{ name }}.js"></script>
+ <link type="text/css" rel="stylesheet" href="http://localhost:{{ port }}/{{ name }}.css">
+ <link href='http://fonts.googleapis.com/css?family={{ font }}' rel='stylesheet' type='text/css'>
+
+ <title>d3py: {{ name }}</title>
</head>
<body>
- <div id="chart"></div>
- <script>
- d3.json("http://localhost:{{ port }}/{{ name }}.json", draw);
- </script>
+ <div id="chart"></div>
+ <script>
+ d3.json("http://localhost:{{ port }}/{{ name }}.json", draw);
+ </script>
</body>
</html>
+
Please sign in to comment.
Something went wrong with that request. Please try again.