{ "cells": [ { "cell_type": "markdown", "metadata": { "deletable": true, "editable": true }, "source": [ "# Tutorial - Drawing Interactive Diagrams with Dual Canvases \n", "\n", "This tutorial explains how to use the `dual_canvas_helper` jQuery plugin to implement\n", "diagrams that respond to mouse events. The tutorial explains how to create the diagrams\n", "as \"standalone pure Javascript\" which can be embedded in any HTML page and also shows\n", "how to implement the diagrams inside a Jupyter IPython notebook.\n", "\n", "The `dual canvas` implementation can be used to develop \n", "complex interactive visualizations such as the \n", "[rectangles proof of concept](./rectangles.ipynb) which presents\n", "a two category bar chart visualization supporting animated repositioning\n", "and mouse over and drag interactions.\n", "\n", "<img src=\"rectangles.png\" width=\"300px\">\n", "\n", "The tutorial is intended for programmers who have some knowledge of Javascript, Python\n", "Jupyter, and HTML 5 libraries such as jQuery. The tutorial also makes use of\n", "[proxy widget features](https://github.com/AaronWatters/jp_proxy_widget) without\n", "explaining them in great detail -- please see the proxy widget tutorial for additional\n", "discussion.\n", "\n", "The dual canvas implementation is a `jQueryUI` plugin which is built using `jQuery`\n", "and some other plugin functionality. It requires several Javascript libraries\n", "to be loaded.\n", "\n", "- `jQuery`,\n", "- `jQueryUI`,\n", "- `js/canvas_2d_widget_helper.js` -- the single canvas `jQuery` plugin, and\n", "- `js/dual_canvas_helper.js` -- the dual canvas `jQuery` plugin.\n", "\n", "The Python wrapper `dual_canvas.py` makes the dual canvas conveniently available\n", "as IPython widgets, and it provides a convenience function for loading the\n", "required javascript libraries: `dual_canvas.load_requirements()`. These requirements\n", "are loaded automagically by the `jp_doodle.dual_canvas.DualCanvasWidget` wrapper widget.\n", "\n", "Below we use `jp_proxy_widget.JSProxyWidget` directly instead of `DualCanvasWidget`\n", "in order to show code fragments that can also run as stand alone Javascript, and that\n", "is why we load the requirements explicitly in the next cell." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false, "deletable": true, "editable": true }, "outputs": [], "source": [ "from jp_doodle import dual_canvas\n", "dual_canvas.load_requirements()" ] }, { "cell_type": "markdown", "metadata": { "deletable": true, "editable": true }, "source": [ "# Drawing objects\n", "\n", "The `dual_canvas_helper` Plugin attaches a factory function to\n", "jQuery elements `element.dual_canvas_helper(config)` which creates\n", "an HTML canvas area associated with the element and attached as a child\n", "of the element. Methods attached to the element like `element.rect({...})`\n", "draw shapes onto the drawing area.\n", "\n", "Below we attach a canvas to a `jp_proxy_widget` IPython widget and\n", "draw a number of objects onto the canvas using Javascript." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false, "deletable": true, "editable": true }, "outputs": [], "source": [ "from IPython.display import HTML, display\n", "import jp_proxy_widget\n", "\n", "# Create a proxy widget to contain the canvas.\n", "canvas0 = jp_proxy_widget.JSProxyWidget()\n", "\n", "# Load javascript for the widget.\n", "canvas0.js_init(\"\"\"\n", "debugger;\n", "// Empty the js_proxy jQuery element associated with the widget.\n", "element.empty();\n", "\n", "// Attach an information div to the element to display event feedback.\n", "var info_area = $(\"<div>Dual canvas feedback will show here.</div>\").appendTo(element);\n", "\n", "// Attach a dual canvas associated with the element as a child of the element\n", "// configured with width 400 and height 200.\n", "var config = {\n", " width: 400,\n", " height: 200,\n", " };\n", "element.dual_canvas_helper(config);\n", "\n", "// Draw some named elements on the canvas.\n", "// A filled yellow circle (disk) named \"Colonel Mustard\n", "element.circle({name: \"Colonel Mustard\", x:100, y:150, r:90, color:\"yellow\"});\n", "\n", "// A filled red rectangle named \"Miss Scarlett\"\n", "element.rect({name: \"Miss Scarlett\", x:100, y:130, w:100, h:20, color: \"red\"});\n", "\n", "// An unfilled white circle named \"Mrs. White\"\n", "element.circle({\n", " name: \"Mrs. White\", x:100, y:150, r:58, fill:false, \n", " color:\"white\", lineWidth: 14});\n", " \n", "// An unfilled blue rectangle named Mrs. Peacock\n", "element.rect({\n", " name: \"Mrs. Peacock\", x:40, y:110, w:100, h:20,\n", " color: \"blue\", lineWidth: 10, degrees:70, fill:false});\n", "\n", "// A line segment named \"Professor Plum\".\n", "element.line({\n", " name: \"Professor Plum\", x1:190, y1:100, x2:10, y2:200,\n", " color:\"purple\", lineWidth: 20})\n", " \n", "// A brown filled polygon (triangle) named Micky\n", "element.polygon({\n", " name: \"Micky\",\n", " points: [[210, 10], [210, 110], [290, 60]],\n", " color: \"brown\",\n", "})\n", "\n", "// A green polyline named Mr. Green\n", "element.polygon({\n", " name: \"Mr. Green\", fill:false, close:false, color: \"green\",\n", " lineWidth: 14, points: [[210, 10], [210, 110], [290, 60]]\n", "})\n", "\n", "// A magenta text string display named Pluto\n", "element.text({\n", " name: \"Pluto\", text: \"The Republic\", font: \"20px Arial\",\n", " x: 20, y:20, degrees: 5, color:\"magenta\"\n", "})\n", "\n", "// Mandrill eyes from a remote image\n", "var mandrill_url = \"http://sipi.usc.edu/database/preview/misc/4.2.03.png\";\n", "//var mandrill_url = \"mandrill.png\"\n", "element.name_image_url(\"mandrill\", mandrill_url);\n", "// just the eyes, not the whole image\n", "element.named_image({\n", " name: \"mandrill eyes\",\n", " image_name: \"mandrill\", x:220, y:170, w:80, h:30,\n", " sx:30, sy:15, sWidth:140, sHeight:20\n", " })\n", "\n", "// Center and scale the figure to fit in the available area.\n", "element.fit()\n", "\n", "// Attach a mouse move event which indicates what object the mouse is over.\n", "var on_mouse_move = function(event) {\n", " if (event.canvas_name) {\n", " info_area.html(\"<div>You are over the object named \" + event.canvas_name + \"</div>\");\n", " } else {\n", " info_area.html(\"<div>You are not over anybody.</div>\");\n", " }\n", "};\n", "element.on_canvas_event(\"mousemove\", on_mouse_move);\n", "\n", "$(\"<div>Please mouse over the canvas area to see event feedback.</div>\").appendTo(element)\n", "\"\"\")\n", "\n", "# Show the canvas\n", "display(canvas0)" ] }, { "cell_type": "markdown", "metadata": { "deletable": true, "editable": true }, "source": [ "# Redraws, animation, and the object list\n", "\n", "The `dual canvas` implementation maintains a sequence of\n", "drawn objects in order to support redrawing the canvas.\n", "Every redraw clears the canvas and then iterates through the object list\n", "to draw each of the objects again in the order they were added to the\n", "canvas.\n", "\n", "Furthermore named objects may be modified or deleted in order\n", "to change the canvas before a redraw. By modifying and\n", "redrawing a canvas a program can implement animations\n", "and other interactive features. Objects\n", "created with no name specified cannot be modified. Changes\n", "to objects or object deletions automatically trigger a canvas\n", "redraw.\n", "\n", "Below we draw a clock face with numbers and add a seconds hand.\n", "We animate rotation for the seconds hand by updating the `degrees` rotation\n", "attribute for each animation frame based on the current time.\n", "\n", "We also add or delete a red circle for a blinking dot effect\n", "every other second. The blinking red circle sits atop a fixed\n", "yellow circle." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false, "deletable": true, "editable": true }, "outputs": [], "source": [ "\n", "# Create a proxy widget to contain the canvas.\n", "canvas1 = jp_proxy_widget.JSProxyWidget()\n", "\n", "# Load javascript for the widget.\n", "canvas1.js_init(\"\"\"\n", "// Empty the js_proxy jQuery element associated with the widget.\n", "element.empty();\n", "\n", "// Attach a dual canvas associated with the element as a child of the element\n", "// configured with width 400 and height 400.\n", "var config = {\n", " width: 400,\n", " height: 400,\n", " };\n", "element.dual_canvas_helper(config);\n", "\n", "// Some math...\n", "five_seconds = Math.PI / 6.0;\n", "twelve_oclock = Math.PI / 2.0;\n", "outer_radius = 100;\n", "inner_radius = 80;\n", "\n", "// Draw the clock face.\n", "element.circle({x:0, y:0, r:100, color:\"#dcf\"});\n", "element.circle({x:0, y:0, r:100, color:\"#449\", fill:false, lineWidth:10});\n", "\n", "// Draw the numbers on the clock.\n", "for (var i=1; i<=12; i++) {\n", " var angle = twelve_oclock - i * five_seconds;\n", " element.text({\n", " text: \"\"+i, font: \"20px Arial\", color: \"#937\", align: \"center\",\n", " x: Math.cos(angle)*inner_radius, y: Math.sin(angle)*inner_radius - 7})\n", "}\n", "\n", "// Draw a \"seconds hand\" and name it so we can change it later.\n", "element.rect({\n", " name: \"seconds hand\",\n", " x:0, y:0, w: inner_radius-15, h:5,\n", " color: \"#937\", degrees: 90});\n", " \n", "// Add a background yellow dot. Note that there is no blinking dot yet.\n", "var dot_visible = false;\n", "element.circle({name: \"background dot\", x:100, y:100, r:12, color:\"yellow\"});\n", "\n", "// Every animation frame, adjust the seconds hand using the time.\n", "// Also add or delete a blinking dot every other second.\n", "var animate = function () {\n", " var seconds = ((new Date()).getTime() * 0.001) % 60;\n", " var degrees = - 6 * seconds;\n", " // Adjust the seconds hand.\n", " element.change_element(\"seconds hand\", {degrees: degrees});\n", " // every other second create or delete the blinking dot\n", " if ((seconds % 2) < 1) {\n", " if (!dot_visible) {\n", " // Add the blinking dot.\n", " element.circle({name: \"blinking dot\", x:100, y:100, r:10, color:\"red\"});\n", " dot_visible = true;\n", " }\n", " } else if (dot_visible) {\n", " // Remove the blinking dot.\n", " element.forget_objects([\"blinking dot\"]);\n", " dot_visible = false;\n", " }\n", " // Repeat the animation again on the next animation iteration.\n", " requestAnimationFrame(animate);\n", "}\n", "// Adjust the canvas coordinate transform to center the drawn objects on the canvas.\n", "element.fit(null, 10); // 10 pixel margin\n", "// Start the animation\n", "animate();\n", "\"\"\")\n", "\n", "display(canvas1)" ] }, { "cell_type": "markdown", "metadata": { "deletable": true, "editable": true }, "source": [ "# The invisible canvas and events\n", "\n", "The implementation is called `dual canvas` because there are actually\n", "two canvas objects created for every `dual canvas` instance: a visible\n", "canvas which shows the drawn objects and an invisible canvas which uses\n", "color indexing tricks to implement mouse event interactions bound\n", "to individual named drawn elements.\n", "\n", "The standard HTML5 canvas does not provide any way to directly associate\n", "a mouse event handler with a drawn figure on a canvas -- only the whole\n", "canvas receives the event.\n", "\n", "The `dual canvas` implementation finds named objects associated with\n", "a mouse click (or other mouse events) by determining the *psuedocolor*\n", "under the mouse click in the invisible canvas and looking up the name of the\n", "object associated with the pseudocolor in an internal data structure.\n", "Objects with no names cannot be bound to events because they don't appear\n", "in the invisible canvas.\n", "\n", "Below we draw some objects and make the invisible canvas visible. The\n", "revealed invisible canvas appears below the visible canvas." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false, "deletable": true, "editable": true }, "outputs": [], "source": [ "# Create a proxy widget to contain the canvas.\n", "canvas2 = jp_proxy_widget.JSProxyWidget()\n", "\n", "# Load javascript for the widget.\n", "canvas2.js_init(\"\"\"\n", "// Empty the js_proxy jQuery element associated with the widget.\n", "element.empty();\n", "\n", "// Attach a dual canvas associated with the element as a child of the element\n", "// configured with width 400 and height 200.\n", "var config = {\n", " width: 200,\n", " height: 100,\n", " };\n", "element.dual_canvas_helper(config);\n", "\n", "// Draw some elements on the canvas.\n", "// A filled yellow circle (disk) named \"Colonel Mustard\n", "element.circle({name: \"Colonel Mustard\", x:100, y:150, r:90, color:\"yellow\"});\n", "// A filled red rectangle named \"Miss Scarlett\"\n", "element.rect({name: \"Miss Scarlett\", x:100, y:130, w:100, h:20, color: \"red\"});\n", "// An unfilled blue rectangle named Mrs. Peacock\n", "element.line({\n", " name: \"Professor Plum\", x1:190, y1:100, x2:10, y2:200,\n", " color:\"purple\", lineWidth: 20})\n", "// A brown filled polygon (triangle) named Micky\n", "element.polygon({\n", " name: \"Micky\",\n", " points: [[210, 10], [210, 110], [290, 60]],\n", " color: \"brown\",\n", "});\n", "// A magenta text named Pluto\n", "element.text({\n", " name: \"Pluto\", text: \"The Republic\", font: \"20px Arial\",\n", " x: 20, y:20, degrees: 5, color:\"magenta\"\n", "});\n", "// Cover part of the canvas with a semi-transparent rectangle with NO NAME\n", "element.rect({x:20, y:20, w:200, h:200, color: \"rgba(100,100,100,0.5)\"});\n", "\n", "element.fit();\n", "\n", "// Reveal the invisible canvas\n", "element.invisible_canvas.show();\n", "\"\"\")\n", "\n", "display(canvas2)" ] }, { "cell_type": "markdown", "metadata": { "deletable": true, "editable": true }, "source": [ "Note that the semi-transparent gray rectangle which obscures much of the\n", "drawing does not appear in the invisible canvas because it has no name.\n", "Also note that the text \"the Republic\" appears as a rectangle in the invisible\n", "canvas to allow the rectangle over the text to associate to the text.\n", "\n", "In the event that there are \"two objects\" under a mouse event only the one\n", "that is \"on top\" will be bound to the event because the invisible canvas\n", "has no knowledge of what object is \"underneath\" the top object.\n", "\n", "# Binding event handlers to the whole canvas\n", "\n", "A program can bind event handlers to the whole canvas. If the event happens\n", "over a named object the event will contain the object name as `event.canvas_name`\n", "and the object description as `event.object_info`.\n", "\n", "\n", "# For example: draggable objects\n", "\n", "Below we draw a number of objects on a canvas and define `mousedown` and `mouseup`\n", "events bound to the whole canvas which \"pick up\" and \"drop\" an object respectively.\n", "If an object has been picked up a `mousemove` event will move the object to the\n", "current cursor location." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false, "deletable": true, "editable": true }, "outputs": [], "source": [ "# Create a proxy widget to contain the canvas.\n", "canvas3 = jp_proxy_widget.JSProxyWidget()\n", "\n", "# Load javascript for the widget.\n", "canvas3.js_init(\"\"\"\n", "// Empty the js_proxy jQuery element associated with the widget.\n", "element.empty();\n", "\n", "// Attach an information div to the element to display event feedback.\n", "var info_area = $(\"<div>Please mouse-down and drag objects below.</div>\").appendTo(element);\n", "\n", "// Attach a dual canvas associated with the element as a child of the element\n", "// configured with width 400 and height 200.\n", "var config = {\n", " width: 400,\n", " height: 200,\n", " };\n", "element.dual_canvas_helper(config);\n", "\n", "// Draw some elements on the canvas.\n", "// A filled yellow circle (disk) named \"Colonel Mustard\n", "element.circle({name: \"Colonel Mustard\", x:100, y:150, r:90, color:\"yellow\"});\n", "// A filled red rectangle named \"Miss Scarlett\"\n", "element.rect({name: \"Miss Scarlett\", x:100, y:130, w:100, h:20, color: \"red\"});\n", "// A magenta text named Pluto\n", "element.text({\n", " name: \"Pluto\", text: \"The Republic\", font: \"20px Arial\",\n", " x: 20, y:20, degrees: 5, color:\"magenta\"\n", "});\n", "\n", "// Mandrill eyes from a remote image\n", "var mandrill_url = \"http://sipi.usc.edu/database/preview/misc/4.2.03.png\";\n", "\n", "// This local link will work in \"classic notebook\" but not Jupyter Lab.\n", "//var mandrill_url = \"mandrill.png\"\n", "// Note: This local image URL reference does not work in Jupyter lab\n", "// you would need to use a URL that references /files/ to make it work.\n", "\n", "element.name_image_url(\"mandrill\", mandrill_url);\n", "// just the eyes, not the whole image\n", "element.named_image({\n", " name: \"mandrill eyes\",\n", " image_name: \"mandrill\", x:220, y:170, w:80, h:30,\n", " sx:30, sy:15, sWidth:140, sHeight:20\n", " })\n", "\n", "// Cover part of the canvas with a semi-transparent rectangle with NO NAME\n", "element.rect({x:20, y:20, w:200, h:200, color: \"rgba(100,100,100,0.5)\"});\n", "\n", "element.fit(null, 10);\n", "\n", "// Define a variable for the picked-up object\n", "var picked_up_object = null;\n", "\n", "// Attach a mousedown event which picks up any named object under the mouse.\n", "var on_mouse_down = function(event) {\n", " if (event.canvas_name) {\n", " info_area.html(\"<div>Picking up the object named \" + event.canvas_name + \"</div>\");\n", " picked_up_object = event.canvas_name;\n", " } else {\n", " info_area.html(\"<div>No object to pick up.</div>\");\n", " }\n", "};\n", "element.on_canvas_event(\"mousedown\", on_mouse_down);\n", "\n", "// Attach a mousemove event which moves any picked up object.\n", "var on_mouse_move = function(event) {\n", " if (picked_up_object) {\n", " var loc = element.event_model_location(event);\n", " info_area.html(\"<div>\"+picked_up_object+\":\"+loc.x+\",\"+loc.y+\"</div>\");\n", " element.change_element(picked_up_object, {\"x\":loc.x, \"y\":loc.y});\n", " } else if (event.canvas_name) {\n", " info_area.html(\"<div>You are over the object named \" + event.canvas_name + \"</div>\");\n", " } else {\n", " info_area.html(\"<div>You are not over anybody.</div>\");\n", " }\n", "};\n", "element.on_canvas_event(\"mousemove\", on_mouse_move);\n", "\n", "// Attach a mouseup event which \"drops\" the current picked up object and re-fits the canvas.\n", "var on_mouse_up = function(event) {\n", " info_area.html(\"<div>Dropping \" + picked_up_object + \".</div>\");\n", " picked_up_object = null;\n", " // refit the canvas to the new object configuration.\n", " element.fit(null, 10)\n", "};\n", "element.on_canvas_event(\"mouseup\", on_mouse_up);\n", "\n", "$(\"<div>Please mouse down and drag over the colorful objects to move them.</div>\")\n", ".appendTo(element);\n", "\"\"\")\n", "\n", "display(canvas3)\n" ] }, { "cell_type": "markdown", "metadata": { "deletable": true, "editable": true }, "source": [ "Note that the gray semi-translucent rectangle does not\n", "respond to events because it has no name, even though\n", "it sits on top of the other objects in the visible canvas.\n", "As an exercise you could rerun the code cell after giving\n", "the gray rectangle a name to see different behavior.\n", "\n", "# Binding event handlers to individual shapes\n", "\n", "In complex visualizations it is convenient to associate\n", "mouse event handlers to drawing components individually.\n", "\n", "Below we create some text elements that behave differently to\n", "click events." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false, "deletable": true, "editable": true }, "outputs": [], "source": [ "# Create a proxy widget to contain the canvas.\n", "canvas4 = jp_proxy_widget.JSProxyWidget()\n", "\n", "# Load javascript for the widget.\n", "canvas4.js_init(\"\"\"\n", "// Empty the js_proxy jQuery element associated with the widget.\n", "element.empty();\n", "\n", "// Attach an information div to the element to display event feedback.\n", "var info_area = $(\"<div>Please click on text areas below.</div>\").appendTo(element);\n", "\n", "// Attach a dual canvas associated with the element as a child of the element\n", "// configured with width 400 and height 200.\n", "var config = {\n", " width: 400,\n", " height: 200,\n", " };\n", "element.dual_canvas_helper(config);\n", "\n", "element.text({\n", " name: \"bigger\", text: \"Click to enlarge\", \n", " font: \"20px Arial\", x: 20, y:20, degrees: 5, color:\"magenta\"\n", "});\n", "var enlarge = function(event) {\n", " element.change_element(\"bigger\", {font: \"40px Arial\"});\n", " // change it back 5 seconds later\n", " setTimeout(function () { element.change_element(\"bigger\", {font: \"20px Arial\"}); }, 5000)\n", "};\n", "element.on_canvas_event(\"click\", enlarge, \"bigger\");\n", "\n", "element.text({\n", " name: \"redden\", text: \"Click to redden\", \n", " font: \"20px Arial\", x: 20, y:50, degrees: 5, color:\"green\"\n", "});\n", "var redden = function(event) {\n", " element.change_element(\"redden\", {color: \"red\"});\n", " // change it back 5 seconds later\n", " setTimeout(function () { element.change_element(\"redden\", {color: \"green\"}); }, 5000)\n", "};\n", "element.on_canvas_event(\"click\", redden, \"redden\");\n", "\n", "element.text({\n", " name: \"font\", text: \"Click to change font\", \n", " font: \"20px Arial\", x: 20, y:80, degrees: 5, color:\"magenta\"\n", "});\n", "var change_font = function(event) {\n", " element.change_element(\"font\", {font: \"20px Courier\"});\n", " // change it back 5 seconds later\n", " setTimeout(function () { element.change_element(\"font\", {font: \"20px Arial\"}); }, 5000)\n", "};\n", "element.on_canvas_event(\"click\", change_font, \"font\");\n", "\n", "element.text({\n", " name: \"rotate\", text: \"Click to rotate\", \n", " font: \"20px Arial\", x: 20, y:120, degrees: 5, color:\"magenta\"\n", "});\n", "var rotate = function(event) {\n", " element.change_element(\"rotate\", {degrees: -15});\n", " // change it back 5 seconds later\n", " setTimeout(function () { element.change_element(\"rotate\", {degrees: 5}); }, 5000)\n", "};\n", "element.on_canvas_event(\"click\", rotate, \"rotate\");\n", "\n", "element.text({\n", " name: \"vanish\", text: \"Click to disappear\", \n", " font: \"20px Arial\", x: 20, y:150, degrees: 5, color:\"magenta\"\n", "});\n", "var disappear = function(event) {\n", " element.change_element(\"vanish\", {hide: true});\n", " // change it back 5 seconds later\n", " setTimeout(function () { element.change_element(\"vanish\", {hide: false}); }, 5000)\n", "};\n", "element.on_canvas_event(\"click\", disappear, \"vanish\");\n", "\"\"\")\n", "\n", "display(canvas4)" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": true, "deletable": true, "editable": true }, "outputs": [], "source": [] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false, "deletable": true, "editable": true }, "outputs": [], "source": [ "# http://population.us/states\n", "# Name population density change\n", "STATES = \"\"\"\n", "Alabama\t4858979\t92.7\t0.329\n", "Alaska\t738432\t1.1\t0.782\n", "Arizona\t6828065\t59.9\t1.329\n", "Arkansas\t2978204\t56\t0.424\n", "California\t39144818\t239.1\t0.995\n", "Colorado\t5456574\t52.4\t1.645\n", "Connecticut\t3590886\t647.8\t0.094\n", "Delaware\t945934\t380.1\t1.047\n", "District of Columbia\t672228\t9836.5\t2.241\n", "Florida\t20271272\t308.3\t1.517\n", "Georgia\t10214860\t171.9\t1.065\n", "Hawaii\t1431603\t131\t1.027\n", "Idaho\t1654930\t19.8\t1.09\n", "Illinois\t12859995\t222.1\t0.046\n", "Indiana\t6619680\t181.8\t0.416\n", "Iowa\t3123899\t55.5\t0.504\n", "Kansas\t2911641\t35.4\t0.407\n", "Kentucky\t4425092\t109.5\t0.392\n", "Louisiana\t4670724\t89.2\t0.599\n", "Maine\t1329328\t37.6\t0.015\n", "Maryland\t6006401\t484.2\t0.794\n", "Massachusetts\t6794422\t643.8\t0.743\n", "Michigan\t9922576\t102.6\t0.079\n", "Minnesota\t5489594\t63.1\t0.691\n", "Mississippi\t2992333\t61.8\t0.168\n", "Missouri\t6083672\t87.3\t0.314\n", "Montana\t1032949\t7\t0.865\n", "Nebraska\t1896190\t24.5\t0.753\n", "Nevada\t2890845\t26.1\t1.371\n", "New Hampshire\t1330608\t142.3\t0.214\n", "New Jersey\t8958013\t1027\t0.375\n", "New Mexico\t2085109\t17.1\t0.251\n", "New York\t19795791\t362.9\t0.427\n", "North Carolina\t10042802\t186.6\t1.042\n", "North Dakota\t756927\t10.7\t2.391\n", "Ohio\t11613423\t259.1\t0.133\n", "Oklahoma\t3911338\t56\t0.839\n", "Oregon\t4028977\t41\t1.012\n", "Pennsylvania\t12802503\t278\t0.157\n", "Rhode Island\t1056298\t683.7\t0.071\n", "South Carolina\t4896146\t152.9\t1.144\n", "South Dakota\t858469\t11.1\t1.065\n", "Tennessee\t6600299\t156.6\t0.789\n", "Texas\t27469114\t102.3\t1.783\n", "Utah\t2995919\t35.3\t1.625\n", "Vermont\t626042\t65.1\t0.01\n", "Virginia\t8382993\t196\t0.937\n", "Washington\t7170351\t100.6\t1.292\n", "West Virginia\t1844128\t76.1\t-0.096\n", "Wisconsin\t5771337\t88.1\t0.295\n", "Wyoming\t586107\t6\t0.785\n", "\"\"\"\n", "\n", "states_mappings = []\n", "for line in STATES.strip().split(\"\\n\"):\n", " sline = line.split(\"\\t\")\n", " name = sline[0]\n", " [pop, density, change] = [float(x) for x in sline[1:]]\n", " states_mappings.append((density, dict(population=pop, density=density, change=change, name=name)))\n", "#states_mappings.sort()\n", "states_mappings.sort(key=lambda x: x[0])\n", "states_mappings.reverse()\n", "states_info = [t[1] for t in states_mappings]\n", "\n", "def extrema(floats):\n", " minimum = min(floats)\n", " maximum = max(floats)\n", " span = maximum - minimum\n", " return dict(minimum=minimum, maximum=maximum, span=span)\n", "\n", "pop_stats = extrema([d[\"population\"] for d in states_info])\n", "density_stats = extrema([d[\"density\"] for d in states_info])\n", "change_stats = extrema([d[\"change\"] for d in states_info])\n", "side = 300.0\n", "# Use x axis as scaled population\n", "x_stats = pop_stats\n", "scale_x = side / pop_stats[\"span\"]\n", "for d in states_info:\n", " d[\"x\"] = d[\"population\"]\n", "# Use y axis as scaled population change\n", "y_stats = change_stats\n", "scale_y = side / change_stats[\"span\"]\n", "for d in states_info:\n", " d[\"y\"] = d[\"change\"]\n", "# Use radius as scaled density\n", "max_radius = 25.0\n", "scale_radius = max_radius / density_stats[\"maximum\"]\n", "for d in states_info:\n", " d[\"r\"] = scale_radius * d[\"density\"] + 5;\n", "# color by all 3\n", "def convert255(value, stats):\n", " return int((value - stats[\"minimum\"]) * 255.0 / stats[\"span\"])\n", "for d in states_info:\n", " r = convert255(d[\"density\"], density_stats)\n", " g = convert255(d[\"population\"], pop_stats)\n", " b = convert255(d[\"change\"], change_stats)\n", " d[\"color\"] = \"rgb(%s,%s,%s)\" % (r,g,b)\n", " \n", " \n", "def state_chart():\n", " chart = jp_proxy_widget.JSProxyWidget()\n", "\n", " # Load javascript for the widget.\n", " chart.js_init(\"\"\"\n", " // Empty the js_proxy jQuery element associated with the widget.\n", " element.empty();\n", "\n", " // Configure a dual canvas\n", " var config = {\n", " width: 400,\n", " height: 400,\n", " };\n", " element.dual_canvas_helper(config);\n", " \n", " element.text({text: \"population\", x:200, y:-100, font: \"20px Arial\", align:\"center\"})\n", " element.text({text: \"% change\", x:-70, y: 200, font: \"20px Arial\", degrees:-90, align:\"center\"})\n", "\n", " var frame = element.frame_region(0, 0, 400, 400, \n", " x_stats.minimum, y_stats.minimum, x_stats.maximum, y_stats.maximum);\n", " //var frame = element.rframe(scale_x, scale_y, \n", " // -x_stats.minimum*scale_x, -y_stats.minimum*scale_y);\n", " //var frame = element.rframe(scale_x, scale_y, 0, 0);\n", " \n", " //debugger;\n", " //element.text({text: \"population\", x:30, y: -30})\n", "\n", " for (var i=0; i<states_info.length; i++) {\n", " frame.circle(states_info[i])\n", " }\n", "\n", " var low_x = x_stats.minimum - 0.05 * x_stats.span;\n", " var low_y = y_stats.minimum - 0.05 * y_stats.span;\n", "\n", " frame.lower_left_axes({\n", " skip_anchor: false,\n", " min_x: low_x, //x_stats.minimum,\n", " max_x: x_stats.maximum,\n", " min_y: low_y, //y_stats.minimum,\n", " max_y: y_stats.maximum\n", " });\n", "\n", " /*\n", " frame.left_axis({\n", " min_value: low_y,\n", " max_value: y_stats.maximum,\n", " axis_origin: {x: low_x, y:0},\n", " add_end_points: true\n", " });\n", "\n", " frame.bottom_axis({\n", " min_value: low_x,\n", " max_value: x_stats.maximum,\n", " axis_origin: {x: 0, y: low_y},\n", " add_end_points: true\n", " });\n", " */\n", "\n", " element.fit()\n", "\n", " \"\"\", states_info=states_info, x_stats=x_stats, y_stats=y_stats, scale_x=scale_x, scale_y=scale_y)\n", " return chart\n", "\n", "canvas5 = state_chart()\n", "display(canvas5)" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false, "deletable": true, "editable": true }, "outputs": [], "source": [ "canvas6 = state_chart()\n", "canvas6.check_jquery()\n", "\n", "canvas6.js_init(\"\"\"\n", "\n", "//debugger;\n", "var dialog = $(\"<div>dialog text</div>\").appendTo(element);\n", "dialog.dialog();\n", "\n", "var mouse_over = function(event) { \n", " var info = event.object_info;\n", " if (info) {\n", " debugger;\n", " // http://api.jqueryui.com/position/\n", " var pos = { my: \"left+5 top+5\", at: \"left bottom\", of: event }\n", " dialog.dialog(\"option\", \"position\", pos);\n", " //dialog.html(\"<div>\" + info.name, + \"</div\n", " var html = (\n", " \"<div>\"+\n", " \"<div>\"+\n", " info.name+\n", " \"</div>\"+\n", " \"<div>density: \"+\n", " info.density+\n", " \"</div>\"+\n", " \"<div>population: \"+\n", " info.population+\n", " \"</div>\"+\n", " \"<div>change: \"+\n", " info.change+\n", " \"</div>\"+\n", " \"</div>\")\n", " dialog.html(html)\n", " dialog.dialog(\"open\");\n", " } else {\n", " dialog.dialog(\"close\");\n", " }\n", "};\n", "\n", "element.on_canvas_event(\"mousemove\", mouse_over);\n", "\n", "dialog.dialog(\"close\");\n", "\n", "\"\"\")\n", "\n", "display(canvas6)" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": true, "deletable": true, "editable": true }, "outputs": [], "source": [] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": true, "deletable": true, "editable": true }, "outputs": [], "source": [] } ], "metadata": { "kernelspec": { "display_name": "Python 2", "language": "python", "name": "python2" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 2 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython2", "version": "2.7.13" } }, "nbformat": 4, "nbformat_minor": 2 }