andy@payne.org
Updated: March 11, 2016
These are my notes on using Javascript inside of the iPython Web Notebook.
The easiest way to access Javascript is with the %%javascript
cell magic:
In [1]:
%%javascript
console.log("Hello World!")
(To view the Javascript console in Chrome & Firefox, use CONTROL+SHIFT+J).
In [2]:
%%javascript
element.append("Hello World!");
(NOTE: prior to version 2.0, there was a container
variable, but the current docs refer to an element
variable. For more information, see the source: outputarea.js).
You can also use the Javascript()
object, creates an object that will be rendered as Javascript by the IPython display system:
In [3]:
from IPython.display import Javascript
Javascript("element.append('Hello World, Again!');")
Out[3]:
A lower-level approach is to implement a _repr_javascript_()
object method that returns Javascript. This lets you add Javascript rendering to any object:
In [4]:
class HelloWorld():
def _repr_javascript_(self):
return "element.append('HelloWorld class');"
hw = HelloWorld()
hw
Out[4]:
In [5]:
%%javascript
window.foovar = 42;
In [6]:
%%javascript
element.append(foovar);
In [7]:
libs = ["https://cdnjs.cloudflare.com/ajax/libs/three.js/r74/three.js"]
Javascript("""element.append(THREE.REVISION);""", lib=libs)
Out[7]:
NOTE: libraries are reloaded each time the cell is run. The underlying implementation, as of IPython 2.1, uses jQuery's getScript() method to load each library in the order specified.
This behavior will break any running event loops (such as an animate event used with THREE.js), because the old event loop will be running with objects defined from the first library load, and the variables will get overwritten with new types based on the second library load.
For example, in this THREE.js example, the animate()
function is called 60 times/second to render the scene and rotate the cube. When the cell is reloaded, the call to render.render(scene, camera)
will fail:
In [8]:
libs = ["https://cdnjs.cloudflare.com/ajax/libs/three.js/r74/three.js"]
Javascript("""
var renderer = new THREE.WebGLRenderer({ antialias: true });
var canvas = renderer.domElement;
element.append(canvas);
var camera = new THREE.PerspectiveCamera(70, canvas.width / canvas.height, 1, 1000);
camera.position.z = 400;
var scene = new THREE.Scene();
var material = new THREE.MeshDepthMaterial();
var mesh = new THREE.Mesh(new THREE.CubeGeometry(200, 200, 200), material);
scene.add(mesh);
function animate() {
renderer.render(scene, camera);
mesh.rotation.x += 0.005;
mesh.rotation.y += 0.01;
requestAnimationFrame(animate);
}
animate();
""", lib=libs)
Out[8]:
Because of this, the animation event loop will stop when the library is reloaded. For example, executing the following cell will cause the rotating cube to stop:
In [9]:
libs = ["https://cdnjs.cloudflare.com/ajax/libs/three.js/r74/three.js"]
Javascript("""console.log(THREE);""", lib=libs)
Out[9]:
A better approach is to use require.js to load external modules, which is supported starting in IPython 2.0. First, define the paths to the different modules:
In [10]:
%%javascript
require.config({
paths: {
'three': "https://cdnjs.cloudflare.com/ajax/libs/three.js/r74/three",
}
});
Now, use the require()
function to wrap your code, specifying the libraries you need:
In [11]:
%%javascript
require(['three'], function() {
element.append(THREE.REVISION);
});
Using the previous example:
In [12]:
%%javascript
var renderer = new THREE.WebGLRenderer({ antialias: true });
var canvas = renderer.domElement;
element.append(canvas);
var camera = new THREE.PerspectiveCamera(70, canvas.width / canvas.height, 1, 1000);
camera.position.z = 400;
var scene = new THREE.Scene();
var material = new THREE.MeshDepthMaterial();
var mesh = new THREE.Mesh(new THREE.CubeGeometry(200, 200, 200), material);
scene.add(mesh);
function animate() {
renderer.render(scene, camera);
mesh.rotation.x += 0.005;
mesh.rotation.y += 0.01;
requestAnimationFrame(animate);
}
animate();
And now reloading doesn't break the animation event loop:
In [13]:
%%javascript
require(['three'], function() {
element.append(THREE.REVISION);
});
In [14]:
import types
from functools import partial
import json
class JSCode(object):
def __init__(self, code):
self.code = code
self.invokes = ""
# Find all the function name( lines in the Javasript code
for line in code.split("\n"):
line = line.strip().split()
if line and line[0] == 'function':
funcname = line[1].split("(")[0]
# For each create a bound method to add to the list of invocations
def invoke(self, *args):
argstr = ','.join([json.dumps(arg) for arg in args])
self.invokes += "%s(%s);\n" % (funcname, argstr)
setattr(self, funcname, types.MethodType(invoke, self))
def _repr_javascript_(self):
"""Return the code definitions and all invocations"""
return self.code + self.invokes
For example:
In [15]:
code = JSCode("""
function test() {
console.log("test!");
}
function output(string) {
element.append(string);
}
""")
Now, the code
object has test()
and output()
methods corresponding to Javascript methods:
In [16]:
code.test, code.output
Out[16]:
When these methods are invoked, corresponding Javascript calls are added to the code
object:
In [17]:
code.output("foo")
code.output(" and bar")
code
Out[17]:
Here is the corresponding Javascript fragment that was generated:
In [18]:
print code._repr_javascript_()
Javascript code for the Notebok "output area": outputarea.js
Relevant pull request and issue: https://github.com/ipython/ipython/pull/4646
In [ ]: