{ "cells": [ { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "slide" } }, "source": [ "# p5.js in the Jupyter Notebook for custom interactive visualizations \n", "\n", "### By Jeremy Tuloup - [@jtpio](https://twitter.com/jtpio) - [jtp.io](https://jtp.io) - [github.com/jtpio](https://github.com/jtpio)\n", "\n", "The goal of this notebook is to show how to create custom visualizations in a Jupyter Notebook using p5.js and Jupyter Widgets. We will make use of existing Javascript libraries and integrate them in the notebook with the help of Jupyter Widgets.\n", "\n", "## The Python Visualization Landscape (2017)\n", "\n", "As you can see on the picture below, there are quite many options to plot data using Python!\n", "\n", "![Python Landscape](./img/python_viz_landscape.png)\n", "\n", "This visualization was created by Nicolas P. Rougier and is based on the talk from Jake VanderPlas at PyCon 2017.\n", "\n", "Source:\n", "\n", "- [Jake VanderPlas: The Python Visualization Landscape PyCon 2017](https://www.youtube.com/watch?v=FytuB8nFHPQ)\n", "- [Source for the Visualization](https://github.com/rougier/python-visualization-landscape), by Nicolas P. Rougier\n", "\n", "\n", "## What is p5.js?\n", "\n", "From the [official website](https://p5js.org):\n", "\n", "> p5.js is a JS client-side library for creating graphic and interactive experiences, based on the core principles of Processing. \n", "\n", "> make coding accessible for artists, designers, educators, and beginners, and reinterprets this for today's web.\n", "\n", "There is also on online editor for creating sketches directly in the browser: \n", "[http://alpha.editor.p5js.org/p5/sketches/S1_gZTpJKIG](http://alpha.editor.p5js.org/p5/sketches/S1_gZTpJKIG)\n", "\n", "![P5 Editor](./img/p5.png)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Running Javascript code in the Jupyter Notebook\n", "\n", "The Jupyter Notebook, like the Python language itself, also comes with batteries included, such as cell magics. One of the cell magics is `%%javascript`, which lets the user run Javascript code directly in the browser.\n", "\n", "As an example, we can define a variable and print its value:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "%%javascript\n", "\n", "let myVar = 'test'\n", "console.log(myVar)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Nothing is shown on the page and it is normal. `console.log` prints in the Developer Tools console. Open it and you should see the content of the variable printed (among other things).\n", "\n", "The Jupyter Notebook also exposes the `element` variable in the Input cell which references the Output cell." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "scrolled": true }, "outputs": [], "source": [ "%%javascript\n", "\n", "element.text('Hello')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Setting up the libraries\n", "\n", "\n", "Jupyter uses [require.js](http://requirejs.org/) under the hood, and the good thing is that we can use of it for our own purposes.\n", "\n", "Using require, we can load the following Javascript libraries:\n", "- p5.js: rendering on the canvas\n", "- lodash: utility library with many high order functions" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "%%javascript\n", "require.config({\n", " paths: {\n", " 'p5': 'https://cdnjs.cloudflare.com/ajax/libs/p5.js/0.6.0/p5.min',\n", " 'lodash': 'https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.4/lodash.min'\n", " }\n", "});" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Jupyter Widgets\n", "\n", "The Jupyter Widgets library is a handy tool to create interactive components in the notebook, simplifying the data flow between the Python kernel and the Javascript frontend:\n", "\n", "![view-model](./img/WidgetModelView.png)\n", "\n", "- Source: https://ipywidgets.readthedocs.io/en/stable/examples/Widget%20Basics.html#Why-does-displaying-the-same-widget-twice-work?\n", "\n", "For more information about Jupyter Widgets, please refer to the [User Guide](https://ipywidgets.readthedocs.io/en/stable/user_guide.html).\n", "\n", "In this example, we will create several widgets, each widget for a specific purpose. To avoid duplicating code, we will start by defining a few helper functions.\n", "\n", "### Helper functions\n", "\n", "The functions defined under `window` will be accessible from anywhere in the notebook when using `%%javascript` cell magic (even in the developer tools console)." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "%%javascript \n", "\n", "window.defineModule = function (name, dependencies, module) {\n", " // force the recreation of the module\n", " // (when re-executing a cell)\n", " require.undef(name);\n", " \n", " define(name, dependencies, module);\n", "};" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "%%javascript\n", "\n", "window.createSketchView = function (name, dependencies, module) {\n", " \n", " require.undef(name);\n", " \n", " define(name,\n", " ['@jupyter-widgets/base', 'p5', 'lodash'].concat(dependencies),\n", " (widgets, p5, _, ...deps) => {\n", "\n", " let viewName = `${name}View`;\n", " \n", " let View = widgets.DOMWidgetView.extend({\n", " initialize: function () {\n", " this.el.setAttribute('style', 'text-align: center;');\n", " },\n", "\n", " render: function () {\n", " // pass the model as the last dependency so it can\n", " // be accessed in the sketch\n", " let sketch = module(...deps, this.model);\n", " setTimeout(() => {\n", " this.sketch = new p5(sketch, this.el); \n", " }, 0);\n", " },\n", "\n", " remove: function () {\n", " // stop the existing sketch when the view is removed\n", " // so p5.js can cancel the animation frame callback and free up resources\n", " if (this.sketch) {\n", " this.sketch.remove();\n", " this.sketch = null;\n", " }\n", " }\n", " });\n", " \n", " return {\n", " [viewName] : View,\n", " };\n", " });\n", "}" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The `defineModule` javascript function can now be used to create a module containing a bunch of parameters (for testing)." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "%%javascript\n", "\n", "// Test module defining a few constants, for example purposes\n", "// Such constants should ideally be defined directly in the model\n", "// and directly accessed by the view\n", "\n", "defineModule('testModule', [], () => {\n", " const [W, H] = [500, 500];\n", " return {W, H};\n", "})" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 2D Demo\n", "\n", "### Defining the view\n", "\n", "Time to define our first view!" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "%%javascript\n", "\n", "createSketchView('Sketch2D', ['testModule'], (TestModule, model) => {\n", " return function(p) {\n", " const {W, H} = TestModule;\n", " const [CX, CY] = [W / 2, H / 2];\n", " \n", " p.setup = function(){\n", " p.createCanvas(W, H);\n", " p.rectMode(p.CENTER);\n", " }\n", "\n", " p.draw = function () {\n", " p.background('#ddd');\n", " p.translate(CX, CY);\n", " let n = model.get('n_squares');\n", " _.range(n).forEach(i => {\n", " p.push();\n", " p.rotate(p.frameCount / 200 * (i + 1));\n", " p.fill(i * 5, i * 100, i * 150);\n", " p.rect(0, 0, 200, 200);\n", " p.pop();\n", " });\n", " }\n", " };\n", "})" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Defining the model\n", "\n", "Now the model can be created on the Python side, by defining a class which inherits from `widgets.DOMWidget`.\n", "The class definition contains a few attributes used to sync data back and forth between the frontend and the backend." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import ipywidgets as widgets\n", "from traitlets import Unicode, Int\n", "\n", "\n", "class Sketch2D(widgets.DOMWidget):\n", " _view_name = Unicode('Sketch2DView').tag(sync=True)\n", " _view_module = Unicode('Sketch2D').tag(sync=True)\n", " _view_module_version = Unicode('0.1.0').tag(sync=True)\n", " n_squares = Int(1).tag(sync=True)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "sketch_2d = Sketch2D()\n", "sketch_2d" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "sketch_2d.n_squares = 4" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 3D Demo\n", "\n", "P5.js even supports basic 3D primitives, so let's create a new widget to test that!" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "%%javascript\n", "\n", "createSketchView('Sketch3D', ['testModule'], (Settings, model) => {\n", " return function(p) {\n", " const {W, H} = Settings;\n", " \n", " p.setup = function(){\n", " p.createCanvas(W, H, p.WEBGL);\n", " }\n", "\n", " p.draw = function () {\n", " p.background('#ddd');\n", " let t = p.frameCount;\n", " let n = model.get('n_cubes');\n", " p.randomSeed(42);\n", " _.range(n).forEach(i => {\n", " const R = 180 //+ 30 * p.sin(t * 0.2 + i);\n", " const x = R * p.cos(i * p.TWO_PI / n);\n", " const y = R * p.sin(i* p.TWO_PI / n);\n", " p.push();\n", " p.translate(x, y);\n", " p.fill(p.random(255), p.random(255), p.random(255));\n", " p.rotateY(t * 0.05 + i);\n", " p.box(50);\n", " p.pop();\n", " });\n", " } \n", " };\n", "})" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "class Sketch3D(widgets.DOMWidget):\n", " _view_name = Unicode('Sketch3DView').tag(sync=True)\n", " _view_module = Unicode('Sketch3D').tag(sync=True)\n", " _view_module_version = Unicode('0.1.0').tag(sync=True)\n", " n_cubes = Int(4).tag(sync=True)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "sketch_3d = Sketch3D()\n", "sketch_3d" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "sketch_3d.n_cubes = 10" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "All of this is great! What about looking at a concrete example?" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "slide" } }, "source": [ "## Concrete example with the Bermuda Triangle Puzzle\n", "\n", "\n", "The Bermuda Triangle Puzzle is a wooden puzzle consisting of 1 big triangle and 16 smaller triangles of equal size.\n", "\n", "On the edges of the big triangle, there are colored dots, as well as on the edges of the small triangles:\n", "\n", "![Bermuda Triangle Puzzle](./img/bermuda_triangle_puzzle.jpg)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Goal\n", "\n", "The 16 triangles all fit inside the big triangle. But the goal of the puzzle is to **arrange the triangles in a way so that each adjacent color is the same: red next to red, blue next to blue, and so on...**.\n", "\n", "One thing to notice is that the colors on the edges of the big triangle cannot be moved, but the smaller triangles can be rotated and re-arranged as we want.\n", "\n", "### How do we solve this?\n", "\n", "As usual, there are different approaches:\n", "\n", "1. One can just take the puzzle and start playing with it. After a while, some patterns may show up but it can become very tedious when trying all combinations.\n", "2. Speaking of trying all combinations, it is also possible to brute force the puzzle manually with the wooden pieces, take notes of the configurations that have been tested, and keep iterating. This requires a lot of rigor to not miss any step and start from the beginning.\n", "3. Brute-forcing can be fun for a moment, but repetitive after a few minutes. Instead of doing it manually, we write a program that goes through all possibilities and try them one by one, maybe finding more than one solution if more exist. This requires knowing how to approach the problem and define an explicit and efficient search strategy, which can be difficult to find and implement.\n", "4. When we don't really know what to do, we can start modelling the puzzle / state, and use stochastic search to find a solution. This requires a set of good building blocks and tools that the stochastic search can rely on. You can find examples of using Simulated Annealing on my [blog](https://jtp.io) to solve the [Aristotle Number Puzzle](https://jtp.io/2017/01/12/aristotle-number-puzzle.html) and the [Nine-Color Cube](https://jtp.io/2017/05/10/nine-color-cube.html).\n", "\n", "### Why a custom visualization?\n", "\n", "Although this problem can be solved without having to create any graphics, the output can be very rudimentary. Basically a list of triangles in a given order. And that's it.\n", "\n", "Without visualization, it can be difficult to verify the correctness of the solution and understand what is happening under the hood.\n", "\n", "That's the whole purpose of this section: being able to solve the problem in Python and visualize the solution, all without leaving the notebook!" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "-" } }, "source": [ "## Strategy\n", "\n", "In this notebook, we will go with the third approach, using a recursive search.\n", "To prove that a solution is correct, the best way is to draw it right away, and visually verify the colors match. This is what we would do with the real puzzle.\n", "\n", "Since we will already be drawing the puzzle, we can push one step further to make an interactive animation of the search.\n", "\n", "Steps:\n", "\n", "1. Model the puzzle by encoding the colors to numbers and arranging them into lists.\n", "2. Define a function, which draws a state on a canvas. A state is defined as the big triangle and a permutation of the 16 small triangles potentially rotated.\n", "3. Define a function, which given a list of actions (permutations of triangles, rotation of triangles), animate them on a canvas.\n", "5. Define a function that uses the helper to search for the solution, logging each step taken (permutation, rotation) to a list of actions.\n", "6. Use the previously defined functions to animated the search and display the final solution." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Model\n", "\n", "This is perhaps the most tedious step, as it requires listing the different pieces with the proper order for the colors.\n", "\n", "### Triangles\n", "\n", "In total, we have 16 triangles, and 6 colors: **white, blue, yellow, green, black and red**." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "slideshow": { "slide_type": "slide" } }, "outputs": [], "source": [ "N_TRIANGLES = 16\n", "IDS = list(range(N_TRIANGLES))\n", "N_COLORS = 6\n", "WHITE, BLUE, YELLOW, GREEN, BLACK, RED = range(N_COLORS)\n", "\n", "# list the pieces, turning anti-clockwise for the colors\n", "# The number at the last position of the tuple indicates the number\n", "# of identical pieces (cf photo above)\n", "triangles_count = [\n", " (WHITE, BLUE, BLUE, 1),\n", " (WHITE, YELLOW, GREEN, 2), # 2 of these\n", " (WHITE, BLACK, BLUE, 2), # 2 of these\n", " (WHITE, GREEN, RED, 1),\n", " (WHITE, RED, YELLOW, 1),\n", " (WHITE, WHITE, BLUE, 1),\n", " (BLACK, GREEN, RED, 1),\n", " (BLACK, RED, GREEN, 2), # 2 of these\n", " (BLACK, BLACK, GREEN, 1),\n", " (BLACK, GREEN, YELLOW, 1),\n", " (BLACK, YELLOW, BLUE, 1),\n", " (GREEN, RED, YELLOW, 1),\n", " (BLUE, GREEN, YELLOW, 1)\n", "]\n", "\n", "assert N_TRIANGLES == sum(t[-1] for t in triangles_count)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The triangles were listed correctly, let's unpack them into a flat list." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "triangles = tuple([t[:-1] for t in triangles_count for times in range(t[-1])])\n", "print(triangles)\n", "\n", "assert N_TRIANGLES == len(triangles)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "This is our immutable list of triangles (tuples), that can be indexed with an integer from 0 to 15.\n", "\n", "### Board\n", "\n", "What needs to be done now is to define the board with the fixed colors. Together with the ordered list of triangles, they define the **state** of the puzzle." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from traitlets import List, Tuple, Dict, validate, default\n", "\n", "class Board(widgets.DOMWidget): \n", " TRIANGLES = List(triangles).tag(sync=True)\n", " LEFT = Tuple((WHITE, RED, WHITE, YELLOW)).tag(sync=True)\n", " RIGHT = Tuple((BLUE, RED, GREEN, BLACK)).tag(sync=True)\n", " BOTTOM = Tuple((GREEN, GREEN, WHITE, GREEN)).tag(sync=True)\n", " \n", " positions = List().tag(sync=True)\n", " permutation = List([]).tag(sync=True)\n", " \n", " @default('positions')\n", " def _default_positions(self):\n", " triangle_id, positions = 0, []\n", " for row in range(4):\n", " n_row = 2 * row + 1\n", " for col in range(n_row):\n", " flip = (triangle_id + row) % 2\n", " positions.append({\n", " 'id': triangle_id,\n", " 'flip': flip,\n", " 'row': row,\n", " 'col': col,\n", " 'n_row': n_row\n", " })\n", " triangle_id += 1\n", " return positions\n", " \n", " @default('permutation')\n", " def _default_permutation(self):\n", " return self.random()\n", " \n", " def random(self):\n", " return [[i, 0] for i in range(N_TRIANGLES)]" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "You might wonder what the `_default_positions` method is really about. What it does is describe where the triangles are positioned by iterating over rows and columns:\n", "\n", "![looping triangle](./img/looping_triangle.png)\n", "\n", "By creating a `Board` we can inspect the value of this property." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "b = Board(permutation=[[i, 0] for i in range(N_TRIANGLES)])\n", "b.positions" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Drawing the state\n", "\n", "We can now reuse the previously defined helpers to create modules and sketches on demand." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "%%javascript\n", "\n", "// define a list of constant such as the size of the base canvas,\n", "// the size of the triangles, colors...\n", "defineModule('settings', [], () => {\n", " const ANIM_W = 800;\n", " const ANIM_H = ANIM_W / 1.6;\n", " const N_TRIANGLES = 16;\n", " const COLORS = ['#fff', '#00f', '#ff0', '#0f0', '#000', '#f00'];\n", " const WOOD_COLOR = '#825201';\n", " const R = 50;\n", " const r = R / 2;\n", " const CR = r / 2;\n", " const OFFSET_X = R * Math.sqrt(3) * 0.5;\n", " const OFFSET_Y = 1.5 * R;\n", " \n", " return {ANIM_W, ANIM_H, N_TRIANGLES, WOOD_COLOR, COLORS, R, r, CR, OFFSET_X, OFFSET_Y};\n", "})" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "With these parameters defined, we can create a `triangles` module to draw one triangle on the screen." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "%%javascript\n", "\n", "defineModule('triangles', ['settings'], (settings) => {\n", " const {COLORS, WOOD_COLOR, R, r, CR, OFFSET_X, OFFSET_Y} = settings;\n", " \n", " function _getPoints(n, startAngle, radius) {\n", " let points = [];\n", " const da = 2 * Math.PI / n;\n", " for (let i = 0; i < n; i++) {\n", " const angle = startAngle - i * da;\n", " const x = radius * Math.cos(angle);\n", " const y = radius * Math.sin(angle);\n", " points.push(x);\n", " points.push(y);\n", " }\n", " return points;\n", " }\n", " \n", " return (p) => {\n", " return {\n", " getTrianglePoints: _getPoints,\n", " getTriangleCoordinates: function (flip, row, col) {\n", " const x = (col - row) * OFFSET_X;\n", " const y = row * OFFSET_Y + ((flip === 1) ? -R/2 : 0);\n", " return {x, y};\n", " },\n", " drawTriangle: function (colors, x, y, rotation, flip=0) {\n", " const n = colors.length;\n", " \n", " p.fill(WOOD_COLOR);\n", " p.push();\n", " p.translate(x, y);\n", " p.rotate(-rotation * p.TWO_PI / 3 + flip * p.PI);\n", " \n", " p.triangle(..._getPoints(n, Math.PI / 6, R));\n", "\n", " let circles = _getPoints(n, Math.PI / 2, 1.25 * CR);\n", " for (let i = 0; i < n; i++) {\n", " const xx = circles[2*i];\n", " const yy = circles[2*i+1];\n", " const color = COLORS[colors[i]];\n", " p.fill(color);\n", " p.ellipse(xx, yy, CR);\n", " }\n", " p.pop();\n", " }\n", " };\n", " };\n", "});" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Static Board\n", "\n", "Let's start small and focus on drawing a static board. This is were the different modules defined above become useful, as we can now depend on them." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "%%javascript\n", "\n", "defineModule('staticBoard', ['settings', 'triangles'], (Settings, Triangles) => {\n", " let {COLORS, R, CR, OFFSET_X, OFFSET_Y} = Settings;\n", " \n", " return (p) => {\n", " let triangles = Triangles(p);\n", " \n", " function _drawStaticColors (left, right, bottom, positions) {\n", " for (let {flip, row, col, n_row} of positions) {\n", " const {x, y} = triangles.getTriangleCoordinates(flip, row, col);\n", " if (col === 0) {\n", " const colorLeft = COLORS[left[row]];\n", " const colorRight = COLORS[right[row]];\n", " p.fill(colorLeft);\n", " p.ellipse(x - OFFSET_X, y - R / 2, CR);\n", " p.fill(colorRight);\n", " p.ellipse(x + n_row * OFFSET_X, y - R / 2, CR);\n", " }\n", " \n", " if (row === 3 && col % 2 == 0) {\n", " p.fill(COLORS[bottom[parseInt(col / 2, 10)]]);\n", " p.ellipse(x, 3.75 * OFFSET_Y, CR);\n", " }\n", " }\n", " }\n", " \n", " function _drawFrame (positions) {\n", " const {flip, row, col} = positions[6];\n", " const {x, y} = triangles.getTriangleCoordinates(flip, row, col);\n", " p.push();\n", " p.noFill();\n", " p.stroke(0);\n", " p.strokeWeight(2);\n", " p.translate(x, y);\n", " p.triangle(...triangles.getTrianglePoints(3, Math.PI / 6, 4 * R));\n", " p.pop();\n", " }\n", " \n", " function _drawTriangles(permutation, triangle_list, positions) {\n", " for (let {id, row, col, flip} of positions) {\n", " const {x, y} = triangles.getTriangleCoordinates(flip, row, col);\n", " let [a, b, c] = triangle_list[permutation[id][0]];\n", " let rot = permutation[id][1];\n", " p.push();\n", " triangles.drawTriangle([a, b, c], x, y, rot, flip);\n", " p.pop();\n", " }\n", " }\n", " \n", " return {\n", " drawStaticColors: _drawStaticColors,\n", " drawFrame: _drawFrame,\n", " drawTriangles: _drawTriangles\n", " };\n", " };\n", "});" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Creating the sketch is now just a matter of putting the previous pieces together:" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "scrolled": false }, "outputs": [], "source": [ "%%javascript\n", "\n", "createSketchView('StaticBoard', ['staticBoard'], (StaticBoard, model) => {\n", " return function(p) {\n", " const W = 400;\n", " const H = 400;\n", " const LEFT = model.get('LEFT');\n", " const RIGHT = model.get('RIGHT');\n", " const BOTTOM = model.get('BOTTOM');\n", " const TRIANGLES = model.get('TRIANGLES');\n", " let staticBoard = StaticBoard(p);\n", "\n", " p.setup = function() {\n", " p.createCanvas(W, H);\n", " }\n", "\n", " p.draw = function () {\n", " p.background('#ddd');\n", " p.push();\n", " p.translate(W / 2, H / 4);\n", " let permutation = model.get('permutation');\n", " let positions = model.get('positions');\n", " staticBoard.drawFrame(positions);\n", " staticBoard.drawTriangles(permutation, TRIANGLES, positions);\n", " staticBoard.drawStaticColors(LEFT, RIGHT, BOTTOM, positions);\n", " p.pop();\n", " }\n", " };\n", "})" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from random import sample\n", "\n", "class StaticBoard(Board):\n", " _view_name = Unicode('StaticBoardView').tag(sync=True)\n", " _view_module = Unicode('StaticBoard').tag(sync=True)\n", " _view_module_version = Unicode('0.1.0').tag(sync=True)\n", " \n", " def __init__(self, **kwargs):\n", " super().__init__(**kwargs)\n", " \n", " def shuffle(self):\n", " self.permutation = sample(self.permutation, N_TRIANGLES) " ] }, { "cell_type": "code", "execution_count": null, "metadata": { "scrolled": false }, "outputs": [], "source": [ "testStaticBoard = StaticBoard()\n", "testStaticBoard" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "testStaticBoard.shuffle()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We can see that all the pieces are drawn, but they are not placed at the correct positions, which is normal since we haven't even tried to solve the problem yet!\n", "\n", "But this is already looking good and this static visualization lets us verify quickly whether or not a solution is valid." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Rotating the triangles\n", "\n", "You must have noticed the number `0` as the last element of the triangle tuple when creating a permutation of the `Board`.\n", "\n", "This number can take the values `[0, 1, 2]` and corresponds to the rotation of a triangle. To make it simple, we order the three colors of the triangles from 0 to 2, with 0 corresponding to the color at the \"bottom\" of the triangle.\n", "\n", "Difficult to visualize? No problem, we can create a new sketch for that purpose!\n", "\n", "Since we want to visualize the rotation of the triangle, let's add another library to our toolbet: [tween.js](https://github.com/tweenjs/tween.js/). With tween.js, we can create tweens which are convenient helpers to create interpolations between values. " ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "%%javascript\n", "\n", "require.config({\n", " paths: {\n", " tween: 'https://cdnjs.cloudflare.com/ajax/libs/tween.js/17.1.1/Tween.min'\n", " }\n", "});" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Now let's define a sketch showcasing the use of tween.js with our triangles." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "%%javascript\n", "\n", "createSketchView('RotateDemo', ['tween', 'triangles'], (Tween, Triangles, model) => {\n", " const [W, H] = [300, 150];\n", " \n", " return (p) => {\n", " let obj = { angle: 0 };\n", " let T = Triangles(p);\n", " \n", " let tweenGroup = new Tween.Group();\n", " let t = new Tween.Tween(obj, tweenGroup)\n", " .to({angle: \"+\" + (p.TWO_PI / 3)}, 500)\n", " .easing(Tween.Easing.Quadratic.InOut)\n", " .onStart(() => t.running = true)\n", " .onComplete(() => t.running = false)\n", " \n", " function rotate () {\n", " if (t.running) return;\n", " t.start();\n", " }\n", " \n", " model.on('change:rotations', rotate);\n", "\n", " p.setup = function(){\n", " p.createCanvas(W, H);\n", " }\n", "\n", " p.draw = function () {\n", " tweenGroup.update();\n", " p.background('#ddd');\n", " p.translate(W / 3, H / 2);\n", " p.push();\n", " p.rotate(obj.angle);\n", " T.drawTriangle([0, 1, 2], 0);\n", " p.pop();\n", " p.push();\n", " p.translate(W / 3, 0);\n", " p.rotate(-obj.angle);\n", " T.drawTriangle([3, 4, 5], 0);\n", " p.pop();\n", " }\n", " };\n", "});" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "class RotateDemo(Board):\n", " _view_name = Unicode('RotateDemoView').tag(sync=True)\n", " _view_module = Unicode('RotateDemo').tag(sync=True)\n", " _view_module_version = Unicode('0.1.0').tag(sync=True)\n", " rotations = Int(0).tag(sync=True)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The Jupyter Widgets ecosystem already provides cool components such as buttons and slider, that we can leverage and connect to our own widget!" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "scrolled": false }, "outputs": [], "source": [ "rotate_button = widgets.Button(description=\"Rotate the triangles\")\n", "\n", "def on_button_clicked(b):\n", " rotate_demo.rotations += 1\n", "\n", "rotate_button.on_click(on_button_clicked)\n", "rotate_demo = RotateDemo()\n", "\n", "widgets.VBox([rotate_button, rotate_demo])" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Animated Board\n", "\n", "As stated above, the goal is to also create an animation of the solving process.\n", "\n", "The purpose is to create an animation that looks like the following:\n", "\n", "![animation_gif](./img/state_animation.gif)\n", "\n", "What we want to visualize is the transition between one state to the next one.\n", "\n", "From:\n", "\n", "![from](./img/anim_from.png)\n", "\n", "to:\n", "\n", "![to](./img/anim_to.png)\n", "\n", "In the end, this will result in creating tweens to interpolate the positions of the triangle at each frame.\n", "\n", "Similar to the previous examples, we can define a new module called `animatedBoard`." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "%%javascript\n", "\n", "defineModule('animatedBoard',\n", " ['settings', 'staticBoard', 'triangles', 'tween', 'lodash'],\n", " (Settings, StaticBoard, Triangles, Tween, _) => {\n", " \n", " const {ANIM_W, ANIM_H, N_TRIANGLES} = Settings;\n", " \n", " return (p, model) => {\n", " let tweenGroup = new Tween.Group();\n", " let [it, globalTime, paused] = [0, 0, false];\n", " \n", " let staticBoard = StaticBoard(p);\n", " let triangles = Triangles(p);\n", " \n", " const TRIANGLES = model.get('TRIANGLES');\n", " const LEFT = model.get('LEFT');\n", " const RIGHT = model.get('RIGHT');\n", " const BOTTOM = model.get('BOTTOM');\n", " \n", " const states = model.get('states');\n", " const positions = model.get('positions');\n", " const out = _.range(N_TRIANGLES).map(i => {\n", " return {\n", " x: ANIM_W * 0.3 + (i % 4) * 100,\n", " y: Math.floor(i / 4) * 100,\n", " r: 0,\n", " f: 0,\n", " };\n", " })\n", " const pos = positions.map(({flip, row, col}) => {\n", " const {x, y} = triangles.getTriangleCoordinates(flip, row, col);\n", " return {x, y, flip};\n", " });\n", " \n", " // arrays of positions to create the tweens (animations)\n", " let [start, end] = [_.cloneDeep(out), _.cloneDeep(out)];\n", " \n", " // store the triangles moving at each turn to display them on top of the others\n", " let moving = [];\n", " \n", " function findPos(triangleId, state) {\n", " return state.findIndex(e => (e && e[0] === triangleId));\n", " }\n", "\n", " function transitionState(curr) {\n", " let [from, to] = [states[curr-1], states[curr]];\n", " to = to || from;\n", " _.range(N_TRIANGLES).forEach(i => {\n", " \n", " const [startPos, endPos] = [findPos(i, from), findPos(i, to)];\n", " \n", " // on the board\n", " if (startPos > -1 && endPos > -1) {\n", " _.assign(start[i], {x: pos[startPos].x, y: pos[startPos].y, r: from[startPos][1], f: pos[startPos].flip});\n", " _.assign(end[i], {x: pos[endPos].x, y: pos[endPos].y, r: to[endPos][1], f: pos[endPos].flip});\n", " return;\n", " }\n", " \n", " // not in current state but in the next one\n", " if (startPos < 0 && endPos > -1) {\n", " _.assign(start[i], {x: out[i].x, y: out[i].y, r: out[i].r, f: out[i].f});\n", " _.assign(end[i], {x: pos[endPos].x, y: pos[endPos].y, r: to[endPos][1], f: pos[endPos].flip});\n", " return;\n", " }\n", " \n", " // in current state but not in the next one, bring back\n", " if (startPos > -1 && endPos < 0) {\n", " _.assign(start[i], {x: pos[startPos].x, y: pos[startPos].y, r: from[startPos][1], f: pos[startPos].flip});\n", " _.assign(end[i], {x: out[i].x, y: out[i].y, r: out[i].r, f: out[i].f});\n", " return;\n", " }\n", " \n", " // out, no movement\n", " if (startPos < 0 && endPos < 0) {\n", " _.assign(start[i], {x: out[i].x, y: out[i].y, r: out[i].r, f: out[i].f});\n", " _.assign(end[i], start[i]);\n", " return;\n", " }\n", " });\n", "\n", " moving = [];\n", " start.forEach((a, i) => {\n", " const b = end[i];\n", " if (a.x != b.x || a.y != b.y || a.r != b.r) {\n", " moving.push(i);\n", " new Tween.Tween(a, tweenGroup)\n", " .to({x: b.x, y: b.y, r: b.r, f: b.f}, model.get('speed') * 0.8)\n", " .easing(Tween.Easing.Quadratic.InOut)\n", " .start(globalTime)\n", " }\n", " }); \n", " }\n", " \n", " model.on('change:frame', () => {\n", " let frame = model.get('frame');\n", " tweenGroup.removeAll();\n", " it = Math.max(1, Math.min(frame, states.length - 1));\n", " transitionState(it);\n", " });\n", " \n", " return {\n", " draw: () => {\n", " tweenGroup.update(globalTime); \n", " globalTime += Math.min(1000 / p.frameRate(), 33);\n", " \n", " p.fill(0);\n", " p.textSize(24);\n", " p.text(`Iteration: ${it}`, 10, 30);\n", " \n", " p.translate(ANIM_W / 4, ANIM_H / 4);\n", " \n", " staticBoard.drawFrame(positions);\n", " \n", " let allTriangles = _.range(N_TRIANGLES);\n", " let staticTriangles = _.difference(allTriangles, moving);\n", " [staticTriangles, moving].forEach(bucket => {\n", " bucket.forEach(triangleId => {\n", " const [a, b, c] = TRIANGLES[triangleId];\n", " const {x, y, r, f} = start[triangleId];\n", " p.push();\n", " triangles.drawTriangle([a, b, c], x, y, r, f);\n", " p.pop();\n", " });\n", " });\n", " staticBoard.drawStaticColors(LEFT, RIGHT, BOTTOM, positions);\n", " }\n", " };\n", " };\n", "});" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "%%javascript\n", "\n", "createSketchView('AnimatedBoard', ['animatedBoard', 'settings'], (AnimatedBoard, Settings, model) => {\n", " const {ANIM_W, ANIM_H} = Settings;\n", "\n", " return function(p) {\n", " let board = AnimatedBoard(p, model);\n", "\n", " p.setup = function () {\n", " p.createCanvas(ANIM_W, ANIM_H);\n", " }\n", "\n", " p.draw = function () {\n", " p.background('#ddd');\n", " board.draw();\n", " }\n", " };\n", "});" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The model is slightly more advanced compared to the previous ones. It contains a few more attributes as well as a `_run` method starting the animation upon request (using a `Thread` from the `threading` module)." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import time\n", "from threading import Thread\n", "from traitlets import Bool, observe\n", "\n", "class AnimatedBoard(Board):\n", " _view_name = Unicode('AnimatedBoardView').tag(sync=True)\n", " _view_module = Unicode('AnimatedBoard').tag(sync=True)\n", " _view_module_version = Unicode('0.1.0').tag(sync=True)\n", " \n", " # animation running automatically\n", " running = Bool(False).tag(sync=True)\n", " # list of states to animate\n", " states = List([]).tag(sync=True)\n", " # current frame (= current iteration)\n", " frame = Int(0).tag(sync=True)\n", " # speed of the animation\n", " speed = Int(1000).tag(sync=True)\n", " \n", " def __init__(self, **kwargs):\n", " super().__init__(**kwargs)\n", " \n", " def next_frame(self):\n", " self.frame = min(self.frame + 1, len(self.states))\n", " \n", " def prev_frame(self):\n", " self.frame = max(0, self.frame - 1)\n", " \n", " @observe('running')\n", " def _on_running_change(self, change):\n", " if change['new']:\n", " # start the animation if going from \n", " # running == False to running == True\n", " self._run()\n", " \n", " def _run(self):\n", " def work():\n", " while self.running and self.frame < len(self.states):\n", " # update the frame number according\n", " # to the speed of the animation\n", " self.frame += 1\n", " time.sleep(self.speed / 1000) \n", " self.running = False\n", "\n", " thread = Thread(target=work)\n", " thread.start()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Let's create a test `AnimatedBoard` for testing with arbitrary states." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "animated_board = AnimatedBoard(\n", " permutation=[[i, 0] for i in range(N_TRIANGLES)],\n", " states=[\n", " [None] * 16,\n", " [[7, 0]] + [None] * 15,\n", " [[7, 1]] + [None] * 15,\n", " [[7, 2]] + [None] * 15,\n", " [[7, 2], [0, 0]] + [None] * 14,\n", " [[7, 2], [0, 1]] + [None] * 14,\n", " [[7, 2], [0, 2]] + [None] * 14,\n", " ]\n", ")\n", "animated_board" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Now we have a canvas but no control. Let's leverage again the existing Jupyter Widgets library to add a few buttons and sliders that will be linked to the `AnimatedBoard` widget." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "scrolled": false }, "outputs": [], "source": [ "from ipywidgets import Layout, Button, Box, VBox, ToggleButton, IntSlider\n", "from traitlets import link\n", "\n", "\n", "def create_animation(animated_board):\n", " items_layout = Layout(flex='flex-stretch', width='auto')\n", "\n", " iteration_slider = IntSlider(max=len(animated_board.states), description='Iteration', layout=Layout(width='100%'))\n", " speed_slider = IntSlider(min=100, max=5000, step=100, description='Speed (ms)')\n", " prev_button = Button(description='◄ Previous', button_style='info')\n", " next_button = Button(description='Next ►', button_style='info')\n", " play_button = ToggleButton(description='Play / Pause', button_style='success', value=False)\n", "\n", " # interactions\n", " link((play_button, 'value'), (animated_board, 'running'))\n", " link((iteration_slider, 'value'), (animated_board, 'frame'))\n", " link((speed_slider, 'value'), (animated_board, 'speed'))\n", " speed_slider.value = 2500\n", " \n", " def on_click_next(b):\n", " animated_board.next_frame()\n", "\n", " def on_click_prev(b):\n", " animated_board.prev_frame()\n", "\n", " next_button.on_click(on_click_next)\n", " prev_button.on_click(on_click_prev)\n", "\n", " box_layout = Layout(display='flex', flex_flow='row', align_items='stretch', width='100%')\n", " items = [play_button, prev_button, next_button, iteration_slider]\n", " box = VBox([\n", " Box(children=items, layout=box_layout), \n", " Box(children=(speed_slider,), layout=box_layout),\n", " animated_board\n", " ])\n", " display(box)" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "scrolled": false }, "outputs": [], "source": [ "create_animation(animated_board)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Recursive Search\n", "\n", "Let's now write the brute-force search using recursion. The overall strategy is to place a triangle and continue placing the following ones until a color mismatches. In that case, the last piece can be removed (backtrack) and replaced by another one.\n", "\n", "A board is valid if there are no conflicting colors. Some slots can be missing (marked as `None`, meaning not placed yet)." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from copy import deepcopy\n", "\n", "\n", "class RecursiveSolver(AnimatedBoard):\n", "\n", " def __init__(self, **kwargs):\n", " super().__init__(**kwargs)\n", " self.reset_state()\n", " \n", " def reset_state(self):\n", " self.board = [None] * N_TRIANGLES\n", " self.used = [False] * N_TRIANGLES\n", " self.logs = [deepcopy(self.board)]\n", " self.it = 0\n", " \n", " def _log(self):\n", " self.logs.append(deepcopy(self.board))\n", " \n", " def _is_valid(self, i):\n", " ts = self.TRIANGLES\n", " permutation, positions = self.board, self.positions[i]\n", " row, col, n_col = positions['row'], positions['col'], positions['n_row']\n", " triangle_id, triangle_rotation = permutation[i]\n", " \n", " # on the left edge\n", " if col == 0 and ts[triangle_id][2-triangle_rotation] != self.LEFT[row]:\n", " return False\n", "\n", " # on the right edge\n", " if col == n_col - 1 and ts[triangle_id][1-triangle_rotation] != self.RIGHT[row]:\n", " return False\n", "\n", " # on the bottom edge\n", " if row == 3 and col % 2 == 0 and ts[triangle_id][0-triangle_rotation] != self.BOTTOM[col//2]:\n", " return False\n", " \n", " if col > 0:\n", " left_pos = i - 1\n", " left_triangle_id, left_triangle_rotation = permutation[left_pos]\n", "\n", " # normal orientation (facing up)\n", " if col % 2 == 0 and ts[triangle_id][2-triangle_rotation] != ts[left_triangle_id][2-left_triangle_rotation]:\n", " return False\n", "\n", " if col % 2 == 1:\n", " # reverse orientation (facing down)\n", " # match with left triangle\n", " if ts[triangle_id][1-triangle_rotation] != ts[left_triangle_id][1-left_triangle_rotation]:\n", " return False\n", " \n", " # match with line above\n", " above_pos = i - (n_col - 1)\n", " above_triangle_id, above_triangle_rotation = permutation[above_pos]\n", " if ts[triangle_id][0-triangle_rotation] != ts[above_triangle_id][0-above_triangle_rotation]:\n", " return False\n", "\n", " return True\n", " \n", " def _place(self, i):\n", " self.it += 1\n", " if i == N_TRIANGLES:\n", " return True\n", " \n", " for j in range(N_TRIANGLES - 1, -1, -1):\n", " if self.used[j]:\n", " # piece number j already used\n", " continue\n", " \n", " self.used[j] = True\n", " \n", " for rot in range(3):\n", " # place the piece on the board\n", " self.board[i] = (j, rot)\n", " self._log()\n", "\n", " # stop the recursion if the current configuration\n", " # is not valid or a solution has been found\n", " if self._is_valid(i) and self._place(i + 1):\n", " return True\n", "\n", " # remove the piece from the board\n", " self.board[i] = None\n", " self.used[j] = False\n", " self._log()\n", " \n", " return False\n", "\n", " def solve(self):\n", " self.reset_state()\n", " self._place(0)\n", " return self.board\n", " \n", " def found(self):\n", " return all(slot is not None for slot in self.board)\n", " \n", " def save_state(self):\n", " self.permutation = self.board\n", " self.states = self.logs" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The most complicated part is probably the `_is_valid` method. What it does is checking that all colors match after placing the last triangle. On the following image good connections have been circled in green and the bad connection in red:\n", "\n", "![valid triangle](./img/valid_triangle.png)\n", "\n", "We can now run the solver and see whether it can find a solution!" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "%%time\n", "\n", "solver = RecursiveSolver()\n", "res = solver.solve()\n", "if solver.found():\n", " print('Solution found!')\n", " print(f'{len(solver.logs)} steps')\n", " solver.save_state()\n", "else:\n", " print('No solution found')" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "solver.permutation" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The solution can be visualized using the existing `StaticBoard` widget." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "scrolled": false }, "outputs": [], "source": [ "static_solution = StaticBoard(permutation=solver.permutation)\n", "static_solution" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Finally, with the real physical pieces:\n", "\n", "![Real Puzzle Solution](./img/bermuda_triangle_solved.jpg)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "But we can also interact with the different steps of the algorithm using the `AnimatedBoard`:" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "scrolled": false }, "outputs": [], "source": [ "create_animation(solver)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "You can also find a short animation here:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from IPython.display import IFrame\n", "\n", "IFrame(src=\"https://www.youtube.com/embed/lW7mo-9TqEQ?rel=0\", width=800, height=500)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Summary\n", "\n", "In this notebook, we managed to combine all the core strengths of Javascript, Python and the Jupyter Notebook to create a custom and interactive walkthrough of the resolution of a puzzle.\n", "\n", "The core logic and the resolution of the problem can be implemented in Python. On the other hand the rendering and visual feedback can be delegated to Javascript (which excels at it), making once again the best out of the tools at our disposition.\n", "Finally, the Jupyter Notebook acts as the glue between the two worlds especially thanks to the wonderful Jupyter Widget library.\n", "\n", "### What have we learned?\n", "\n", "1. How to create a Jupyter Widget on the fly in the Jupyter Notebook, and use is to transfer data between Python and Javascript\n", "2. Use p5.js to create a custom animation in HTML5 and Javascript\n", "\n", "### Applications\n", "\n", "Such interactive notebooks and the possibility to leverage existing Javascript frameworks can be used in many applications: \n", "\n", "- Visual debugging, or visually understanding a complex system\n", "- Teaching and education\n", "- Combine HTML5 / Javascript games with data\n", "\n", "### Improvements\n", "\n", "Even though the end result is quite satisfying, there are a couple of things that can be improved for this workflow:\n", "\n", "- The setup code and the animations were tailored for this specific problem and not really designed with reusability in mind. One could built a mini-libraries of such helper functions on the side that can be imported in the notebook (as Python packages or as a packaged Jupyter Widget).\n", "- Does it work in JupyterLab? For [security reasons](https://github.com/jupyterlab/jupyterlab/issues/3118), JupyterLab prevents the execution of Javascript code in a cell (basically what we did in the entire notebook). The way to go with JupyterLab would be to package the widget and deliver it as a proper extension. A much cleaner and production ready strategy than writing everything on the fly in a notebook!\n", "- Depending on where you run the notebook, and most importantly on where the kernel is run, you might notice some jitter and latency in the animation. This is due to the round trip between the client (web browser) and the server (Python kernel), and is further explained in the documentation: https://ipywidgets.readthedocs.io/en/stable/examples/Widget%20Events.html?highlight=link#Linking-widgets-attributes-from-the-client-side. Depending on the application it might or might not be a problem.\n", "\n", "## References\n", "\n", "I underwent quite a lot of research to find an existing library that would fit my needs (low level rendering to draw custom shapes, with interactivity). There are already some projects implementing similar use cases, but all still specific in some way. Here is a list of useful references for a more advanced understanding of the entire ecosystem and know where the Jupyter Widgets fit on the overall picture:\n", "\n", "- [Jake VanderPlas: The Python Visualization Landscape PyCon 2017](https://www.youtube.com/watch?v=FytuB8nFHPQ)\n", "- Drawing from the presentation made by Nicolas P. Rougier: https://github.com/rougier/python-visualization-landscape\n", "- [Building a Custom Widget - Hello World - Tutorial](https://ipywidgets.readthedocs.io/en/stable/examples/Widget%20Custom.html)\n", "- [Sylvain Corlay - Interactive Visualization in Jupyter with Bqplot and Interactive Widgets - PyData London 2016](https://www.youtube.com/watch?v=eVET9IYgbao)\n", "- [pythreejs](https://github.com/jovyan/pythreejs): Implemented as an Jupyter Widget\n", "- [bqplot](https://github.com/bloomberg/bqplot): Great library for interactive data exploration\n", "- [ipyvolume](https://github.com/maartenbreddels/ipyvolume): 3d plotting for Python in the Jupyter notebook\n", "\n", "## Conclusion\n", "\n", "Let's create a **pyp5** Python package? `¯\\_(ツ)_/¯`" ] } ], "metadata": { "anaconda-cloud": {}, "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.7.1" } }, "nbformat": 4, "nbformat_minor": 1 }