{ "metadata": { "name": "" }, "nbformat": 3, "nbformat_minor": 0, "worksheets": [ { "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "The Bokeh Plot Library\n", "======================\n", "\n", "I am [paddy_mullen](https://twitter.com/paddy_mullen). I work for [Continuum Analytics](http://continuum.io/) where we write the [Bokeh](http://bokeh.pydata.org) open source plotting library. This tutorial will walk you through the basic bokeh plotting api and show you some of the advanced possiblilities. Peter Wang, Bryan Van de Ven, Hugo Shi and myself are the primary contributors.\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Installation instructions for tutorial\n", "======================================\n", "\n", "If you have conda installed run the following shell commands\n", "\n", " mkdir bokeh_example\n", " cd bokeh_example/\n", " git clone https://github.com/paddymul/bokeh_tutorial.git\n", " conda create -n bokeh_tutorial bokeh ipython-notebook pyyaml pyaudio anaconda=1.8 --yes\n", " source activate bokeh_tutorial\n", " cd bokeh_tutorial\n", " ipython notebook\n", "\n", "Then in the IPython notebook, open the bokeh_tutorial notebook.\n", "\n", "If you are executing this notebook, please use the menu and select Cell -> All Output -> Clear. Then reload the page, this quirk will be going away in 0.3." ] }, { "cell_type": "code", "collapsed": false, "input": [ "import numpy as np\n", "from bokeh.plotting import output_notebook\n", "import pandas as pd\n", "output_notebook()\n" ], "language": "python", "metadata": {}, "outputs": [ { "html": [ "
\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "

Configuring embedded BokehJS mode.

\n", " \n", "
" ], "metadata": {}, "output_type": "display_data" } ], "prompt_number": 1 }, { "cell_type": "markdown", "metadata": {}, "source": [ "Here is a simple plot." ] }, { "cell_type": "code", "collapsed": false, "input": [ "from bokeh.plotting import line, show\n", "x = np.linspace(0, 4*np.pi, 20)\n", "y = np.sin(x)\n", "line(x,y, color=\"#0000FF\", tools=[])\n", "show()" ], "language": "python", "metadata": {}, "outputs": [ { "html": [ "
\n", " \n", " \n", " \n", " \n", "
Plots
\n", "\n", " \n", "
" ], "metadata": {}, "output_type": "display_data" } ], "prompt_number": 3 }, { "cell_type": "markdown", "metadata": {}, "source": [ "Take a moment to play around with a simple line plot.\n" ] }, { "cell_type": "code", "collapsed": false, "input": [ "from bokeh.plotting import line\n", "import numpy as np\n", "x = np.linspace(0, 4*np.pi, 20)\n", "y = np.sin(x)\n", "line(x,y, color=\"#0000FF\", tools=[], plot_width=400, plot_height=400)\n", "show()" ], "language": "python", "metadata": {}, "outputs": [ { "html": [ "
\n", " \n", " \n", " \n", " \n", "
Plots
\n", "\n", " \n", "
" ], "metadata": {}, "output_type": "display_data" } ], "prompt_number": 4 }, { "cell_type": "markdown", "metadata": {}, "source": [ "Let's try some glyphs\n", "=====================\n", "\n", "Bokeh is built around glyphs and plot objects that can be composed into plots. This is simple and powerful. The whole system can be manipulated from python without the need to write javascript or html code.\n", "\n", "[Gallery](http://bokeh.pydata.org/gallery.html)\n", "===============================================" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "There are many many types of glyphs:\n", "Here is a list\n", "\n", "- line \n", "- multi_line \n", "- annular_wedge \n", "- annulus \n", "- arc \n", "- bezier \n", "- oval \n", "- patch \n", "- patches \n", "- ray \n", "- quad \n", "- quadratic \n", "- rect \n", "- segment \n", "- text \n", "- wedge \n", "\n", "Let's look at combining two glyph renderers onto the same plot.\n", "\n", "To do this we use the `hold() ` function, this allows us to combine renderers onto the same plot." ] }, { "cell_type": "code", "collapsed": false, "input": [ "from bokeh.plotting import rect\n", "rect([10,20,30], [10,20,30], width=2, height=5, plot_width=400, plot_height=400, tools=[])\n", "show()" ], "language": "python", "metadata": {}, "outputs": [ { "html": [ "
\n", " \n", " \n", " \n", " \n", "
Plots
\n", "\n", " \n", "
" ], "metadata": {}, "output_type": "display_data" } ], "prompt_number": 5 }, { "cell_type": "markdown", "metadata": {}, "source": [ "Lets try combining glyphs:\n", "==========================\n", "\n", "Due to a bug multiple plots will show up here, just look at the first two." ] }, { "cell_type": "code", "collapsed": false, "input": [ "from bokeh.plotting import annular_wedge, hold, figure, show\n", "figure() #create a new figure\n", "hold(False)\n", "\n", "annular_wedge(\n", " [10,20,30], [30,25,10], 10, 20, 0.6, 4.1,\n", " inner_radius_units=\"screen\", outer_radius_units = \"screen\",\n", " color=\"#8888ee\", tools=[])\n", "hold(True)\n", "rect([10,20,30], [10,20,30], width=2, height=5, plot_width=400, plot_height=400, tools=[])\n", "show()" ], "language": "python", "metadata": {}, "outputs": [ { "html": [ "
\n", " \n", " \n", " \n", " \n", "
Plots
\n", "\n", " \n", "
" ], "metadata": {}, "output_type": "display_data" } ], "prompt_number": 6 }, { "cell_type": "markdown", "metadata": {}, "source": [ "So at this point we have a very flexible powerful plotting system. What about our data ranges?\n", "They are automatically configured for us. Notice that we don't have to specify pixels, only data sizes." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Tools\n", "=====\n", "\n", "Bokeh ships with existing tools for pan, zoom, preview save, resize, and embed. \n", "Tools are added with the tools kwarg of plots, like this:" ] }, { "cell_type": "code", "collapsed": false, "input": [ "hold(False)\n", "x = np.linspace(0, 4*np.pi, 20)\n", "y = np.sin(x)\n", "#use a scatter because select doesn't work on lines\n", "line(x,y, color=\"#0000FF\", tools=\"pan, zoom, resize, select, save\")\n", "show()" ], "language": "python", "metadata": {}, "outputs": [ { "html": [ "
\n", " \n", " \n", " \n", " \n", "
Plots
\n", "\n", " \n", "
" ], "metadata": {}, "output_type": "display_data" } ], "prompt_number": 7 }, { "cell_type": "markdown", "metadata": {}, "source": [ "BokehJS object graph\n", "====================\n", "\n", "Bokehjs renders plots based on their object graph. Inside this graph objects like renderers are described.\n", "\n", "Plots have renderers, axes, grids, and tools. Renderers (Circle, Quad, Line..) have references to DataRanges and DataSources.\n", "Data Ranges operate on a DataSource to describe which portion of the dataspace should be rendered.\n", "Data sources containe the actual data to be displayed. multiple columns are algined on the same x-axis in data sources.\n", "\n", "Now here is the really cool thing. Since plots don't have an attribute of min x and max x, but instead they have a reference to a data range, two separate plots can share the same data range. This means that they will pan and zoom together.\n" ] }, { "cell_type": "code", "collapsed": false, "input": [ "from IPython.display import Image\n", "Image(filename='bokeh_objects.png')" ], "language": "python", "metadata": {}, "outputs": [] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Let's see what a simple line plot looks like if we build the object graph up" ] }, { "cell_type": "code", "collapsed": false, "input": [ "from numpy import pi, arange, sin, cos\n", "import numpy as np\n", "import os.path\n", "\n", "from bokeh.objects import (Plot, DataRange1d, LinearAxis, \n", " ObjectArrayDataSource, ColumnDataSource, Glyph, GridPlot,\n", " PanTool, ZoomTool)\n", "from bokeh.glyphs import Line, Rect\n", "from bokeh import session\n", "x = np.linspace(-2*pi, 2*pi, 100)\n", "y = sin(x)\n", "z = cos(x)\n", "widths = np.ones_like(x) * 0.02\n", "heights = np.ones_like(x) * 0.2" ], "language": "python", "metadata": {}, "outputs": [], "prompt_number": 9 }, { "cell_type": "code", "collapsed": false, "input": [ "#I'm putting all of this into a function so that we don't pollute the global namespace\n", "def simple_line_object():\n", " from bokeh.plotting import curplot\n", " source = ColumnDataSource(data=dict(x=x,y=y,z=z,widths=widths,\n", " heights=heights))\n", " xdr = DataRange1d(sources=[source.columns(\"x\")])\n", " ydr = DataRange1d(sources=[source.columns(\"y\")])\n", " line_glyph = Line(x=\"x\", y=\"y\", line_color=\"blue\")\n", " renderer = Glyph(data_source = source,\n", " xdata_range = xdr, ydata_range = ydr,\n", " glyph = line_glyph)\n", "\n", " plot = Plot(x_range=xdr, y_range=ydr, data_sources=[source], \n", " border=50, height=300, width=300)\n", " xaxis = LinearAxis(plot=plot, dimension=0, location=\"bottom\")\n", " yaxis = LinearAxis(plot=plot, dimension=1, location=\"left\")\n", "\n", " pantool = PanTool(dataranges = [xdr, ydr], dimensions=[\"width\",\"height\"])\n", " zoomtool = ZoomTool(dataranges=[xdr,ydr], dimensions=(\"width\",\"height\"))\n", "\n", " plot.renderers.append(renderer)\n", " plot.tools = [pantool, zoomtool]\n", " sess = curplot()._session\n", " sess.add(plot, renderer, xaxis, yaxis, source, xdr, ydr, pantool, zoomtool)\n", " sess.plotcontext.children.append(plot)\n", " \n", "simple_line_object()\n", "show()" ], "language": "python", "metadata": {}, "outputs": [ { "html": [ "
\n", " \n", " \n", " \n", " \n", "
Plots
\n", "\n", " \n", "
" ], "metadata": {}, "output_type": "display_data" } ], "prompt_number": 16 }, { "cell_type": "markdown", "metadata": {}, "source": [ "Linked panning\n", "==============\n", "Now we will create two plots which share the same DataRange object" ] }, { "cell_type": "code", "collapsed": false, "input": [], "language": "python", "metadata": {}, "outputs": [ { "metadata": {}, "output_type": "pyout", "prompt_number": 14, "text": [ "" ] } ], "prompt_number": 14 }, { "cell_type": "code", "collapsed": false, "input": [ "from bokeh.glyphs import Wedge, Rect\n", "def simple_linked():\n", " from bokeh.plotting import curplot\n", " source = ColumnDataSource(data=dict(x=x,y=y,z=z,widths=widths,\n", " heights=heights))\n", "\n", " xdr = DataRange1d(sources=[source.columns(\"x\")])\n", " ydr = DataRange1d(sources=[source.columns(\"y\")])\n", "\n", " line_glyph = Line(x=\"x\", y=\"y\", line_color=\"blue\")\n", " #FIXME, I can't seem to get other glyph styles to work\n", " rect_glyph = Rect(x=\"x\", y=\"y\", height=.5, width=.05, angle=30)\n", " wedge_glyph = Wedge(x=\"x\", y=\"y\", radius=np.pi/4, \n", " start_angle= np.pi/6, end_angle=np.pi/2, direction=\"clock\", color=\"red\")\n", "\n", " renderer = Glyph(data_source = source, xdata_range = xdr,\n", " ydata_range = ydr, glyph = line_glyph)\n", " \n", " plot = Plot(x_range=xdr, y_range=ydr, data_sources=[source], \n", " border=50, height=300, width=300)\n", " plot.renderers.append(renderer)\n", "\n", " renderer2 = Glyph(data_source = source, xdata_range = xdr,\n", " ydata_range = ydr, glyph = line_glyph)\n", "\n", " plot2 = Plot(x_range=xdr, y_range=ydr, data_sources=[source], \n", " border=50, height=300, width=300)\n", " pantool2 = PanTool(dataranges = [xdr, ydr], dimensions=[\"width\",\"height\"])\n", " zoomtool2 = ZoomTool(dataranges=[xdr,ydr], dimensions=(\"width\",\"height\"))\n", "\n", " plot2.renderers.append(renderer2)\n", " plot2.tools = [pantool2, zoomtool2]\n", "\n", " sess = curplot()._session\n", " sess.add(plot, renderer, source, xdr, ydr)\n", " sess.plotcontext.children.append(plot)\n", " show()\n", " sess.add(plot2, renderer2, pantool2, zoomtool2)\n", " sess.plotcontext.children.append(plot2)\n", "simple_linked()\n", "show()" ], "language": "python", "metadata": {}, "outputs": [ { "html": [ "
\n", " \n", " \n", " \n", " \n", "
Plots
\n", "\n", " \n", "
" ], "metadata": {}, "output_type": "display_data" }, { "html": [ "
\n", " \n", " \n", " \n", " \n", "
Plots
\n", "\n", " \n", "
" ], "metadata": {}, "output_type": "display_data" } ], "prompt_number": 18 }, { "cell_type": "code", "collapsed": false, "input": [ "\n", "def line_advanced():\n", " from bokeh.plotting import curplot\n", " source = ColumnDataSource(data=dict(x=x,y=y,z=z,widths=widths,\n", " heights=heights))\n", " \n", " xdr = DataRange1d(sources=[source.columns(\"x\")])\n", " xdr2 = DataRange1d(sources=[source.columns(\"x\")])\n", " ydr = DataRange1d(sources=[source.columns(\"y\")])\n", " ydr2 = DataRange1d(sources=[source.columns(\"y\")])\n", " \n", " line_glyph = Line(x=\"x\", y=\"y\", line_color=\"blue\")\n", " wedge_glyph = Wedge(x=\"x\", y=\"y\", radius=np.pi/14, \n", " start_angle= 3*np.pi/6, end_angle=4*np.pi/4, direction=\"clock\")\n", " \n", " renderer = Glyph(data_source = source, xdata_range = xdr,\n", " ydata_range = ydr, glyph = line_glyph)\n", " pantool = PanTool(dataranges = [xdr, ydr], dimensions=[\"width\",\"height\"])\n", " zoomtool = ZoomTool(dataranges=[xdr,ydr], dimensions=(\"width\",\"height\"))\n", " \n", " plot = Plot(x_range=xdr, y_range=ydr, data_sources=[source], \n", " border=50, height=400, width=400)\n", " plot.tools = [pantool, zoomtool]\n", " plot.renderers.append(renderer)\n", " \n", " #notice that these two have a different y data range\n", " renderer2 = Glyph(data_source = source, xdata_range = xdr,\n", " ydata_range = ydr2, glyph = line_glyph)\n", " \n", " plot2 = Plot(x_range=xdr, y_range=ydr2, data_sources=[source], \n", " border=50, height=400, width=400)\n", " \n", " plot2.renderers.append(renderer2)\n", " \n", " #notice that these two have a differen y data range\n", " renderer3 = Glyph(data_source = source, xdata_range = xdr2,\n", " ydata_range = ydr, glyph = line_glyph)\n", " \n", " plot3 = Plot(x_range=xdr2, y_range=ydr, data_sources=[source], \n", " border=50, height=400, width=400)\n", " \n", " plot3.renderers.append(renderer3)\n", " \n", " #this is a dummy plot with no renderers\n", " plot4 = Plot(x_range=xdr2, y_range=ydr, data_sources=[source], \n", " border=50, height=400, width=400)\n", " \n", " \n", " sess = curplot()._session\n", " sess.add(plot, renderer, source, xdr, ydr, pantool, zoomtool)\n", " \n", " sess.add(plot2, renderer2, ydr2, xdr2, renderer3, plot3, plot4)\n", " grid = GridPlot(children=[[plot, plot2], [plot3, plot4 ]], name=\"linked_advanced\")\n", " \n", " sess.add(grid)\n", " sess.plotcontext.children.append(grid)\n", "line_advanced()\n", "show()" ], "language": "python", "metadata": {}, "outputs": [ { "html": [ "
\n", " \n", " \n", " \n", " \n", "
Plots
\n", "\n", " \n", "
" ], "metadata": {}, "output_type": "display_data" } ], "prompt_number": 19 }, { "cell_type": "markdown", "metadata": {}, "source": [ "\n", "There are 3 distinct components to the bokeh plotting library.\n", "\n", "\n", "- The python bokeh client library. This the api that we are using in the talk to generate plots\n", "- The bokeh plot server. This keeps track of which plots are in which documents,\n", " and its a webserver that communicates with the browser.\n", "- The bokehjs javascript library. This renders the plots and communicates with the plot server.\n", "\n", "This archictecture lets us do some remarkable things.\n", "\n", "It is possible to run bokeh without the plot server. The file based examples that we have seen output static javascript that includes everything needed for bokehjs to display the plot. It is also important to understand that 99% of bokeh stays the same however the plot is output.\n", "\n", "Embedding\n", "=========\n" ] }, { "cell_type": "code", "collapsed": false, "input": [ "from bokeh.plotting import hold, line\n", "hold(False)\n", "x = np.linspace(0, 4*np.pi, 20)\n", "y = np.sin(x)\n", "hold(True)\n", "line_plot = line(x,y, color=\"#0000FF\", tools=\"pan, zoom, preview, resize, select, embed, save\")\n", "\n", "line_snippet = line_plot.inject_snippet()\n", "print line_snippet\n", "hold(False)" ], "language": "python", "metadata": {}, "outputs": [] }, { "cell_type": "code", "collapsed": false, "input": [ "import webbrowser\n", "import os\n", "#ok let's create an html page with that snippet\n", "\n", "open(\"foo.html\",\"w\").write(\"\"\"\n", "\n", "\n", "

Embed example

\n", "%s\n", "

after embed

\n", "\n", "\"\"\" % line_snippet)\n", "\n", "webbrowser.open(\"file://\" + os.path.abspath(\"foo.html\"))\n" ], "language": "python", "metadata": {}, "outputs": [] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Animation\n", "=========\n", "Since plots are first class objects in bokehjs and the bokeh python system they can be modified. Because the bokeh plotserver communicates updates to the browser, we can animate plots from python. For these demos to work, you must be running the plot server.\n", "\n", " $ bokeh-server\n", "\n", "The bokeh plot server does not yet work on windows.\n", "Once you have the started the server, navigate to the [plot server http://localhost:5006/bokeh](http://localhost:5006/bokeh) in another browser tab. Due to a bug in bokeh, all of the plots created start out zoomed in, you must zoom out to see the whole animation.\n", "\n", "The IPython kernel runs the animation, to interupt the kernel type `CTRL-m i`." ] }, { "cell_type": "code", "collapsed": false, "input": [ "print \"Go to http://localhost:5006/bokeh to view this plot\"\n", "\n", "import numpy as np\n", "from numpy import pi, cos, sin, linspace\n", "from bokeh.plotting import *\n", "\n", "colors = (\"#A6CEE3\", \"#1F78B4\", \"#B2DF8A\")\n", "N = 36\n", "r_base = 8\n", "theta = linspace(0, 2*pi, N)\n", "r_x = linspace(0, 6*pi, N-1)\n", "rmin = r_base - cos(r_x) - 1\n", "rmax = r_base + sin(r_x) + 1\n", "\n", "output_server(\"wedge animate\")\n", "\n", "cx = cy = np.ones_like(rmin)\n", "annular_wedge(cx, cy, \n", " rmin, rmax, theta[:-1], theta[1:],\n", " inner_radius_units=\"data\",\n", " outer_radius_units=\"data\",\n", " color = colors[0], \n", " line_color=\"black\", tools=\"pan,zoom,resize\")\n", "#show()\n", "\n", "import time\n", "from bokeh.objects import GlyphRenderer\n", "renderer = [r for r in curplot().renderers if isinstance(r, GlyphRenderer)][0]\n", "ds = renderer.data_source\n", "while True:\n", " for i in np.linspace(-2*np.pi, 2*np.pi, 50):\n", " rmin = ds.data[\"inner_radius\"]\n", " rmin = np.roll(rmin, 1)\n", " ds.data[\"inner_radius\"] = rmin\n", " rmax = ds.data[\"outer_radius\"]\n", " rmax = np.roll(rmax, -1)\n", " ds.data[\"outer_radius\"] = rmax\n", " ds._dirty = True\n", " session().store_obj(ds)\n", " time.sleep(.25)\n" ], "language": "python", "metadata": {}, "outputs": [] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Spectrogram demo\n", "================" ] }, { "cell_type": "code", "collapsed": false, "input": [ "import numpy as np\n", "from numpy import pi, cos, sin, linspace, zeros, linspace, \\\n", " short, fromstring, hstack, transpose\n", "from scipy import fft\n", "import time\n", "from bokeh.plotting import *\n", "\n", "NUM_SAMPLES = 1024\n", "SAMPLING_RATE = 44100\n", "MAX_FREQ = SAMPLING_RATE / 8\n", "FREQ_SAMPLES = NUM_SAMPLES / 8\n", "SPECTROGRAM_LENGTH = 400\n", "\n", "_stream = None\n", "def read_mic():\n", " import pyaudio\n", " global _stream\n", " if _stream is None:\n", " pa = pyaudio.PyAudio()\n", " _stream = pa.open(format=pyaudio.paInt16, channels=1, rate=SAMPLING_RATE,\n", " input=True, frames_per_buffer=NUM_SAMPLES)\n", " try:\n", " audio_data = fromstring(_stream.read(NUM_SAMPLES), dtype=short)\n", " normalized_data = audio_data / 32768.0\n", " return (abs(fft(normalized_data))[:NUM_SAMPLES/2], normalized_data)\n", " except:\n", " return None\n", "\n", "def get_audio_data(interval=0.05):\n", " time.sleep(interval)\n", " starttime = time.time()\n", " while time.time() - starttime < interval:\n", " data = read_mic()\n", " if data is not None:\n", " return data\n", " return None\n", "\n", "output_server(\"spectrogram\")\n", "\n", "# Create the base plot\n", "N = 36\n", "theta = linspace(0, 2*pi, N+1)\n", "rmin = 10\n", "rmax = 20 * np.ones(N)\n", "cx = cy = np.ones(N)\n", "annular_wedge(cx, cy, rmin, rmax, theta[:-1], theta[1:],\n", " inner_radius_units = \"data\",\n", " outer_radius_units = \"data\",\n", " color = \"#A6CEE3\", line_color=\"black\", \n", " tools=\"pan,zoom,resize\")\n", "show()\n", "\n", "from bokeh.objects import GlyphRenderer\n", "renderer = [r for r in curplot().renderers if isinstance(r, GlyphRenderer)][0]\n", "ds = renderer.data_source\n", "while True:\n", " data = get_audio_data()\n", " if data is None:\n", " continue\n", " else:\n", " data = data[0]\n", " # Zoom in to a frequency range:\n", " data = data[:len(data)/2]\n", " histdata = (np.histogram(data, N, density=True)[0] * 5) + rmin\n", " ds.data[\"outer_radius\"] = histdata\n", " ds._dirty = True\n", " session().store_obj(ds)\n" ], "language": "python", "metadata": {}, "outputs": [] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Future of Bokeh\n", "===============\n", "\n", "There are a lot of exciting things in store for bokeh. These include:\n", "\n", "- better IPython notebook support. We are looking to integrate with their new widget model.\n", "- better embedding support\n", "- built in s3 uploading\n", "- static animation that doesn't require the plot server\n", "- performance enhancements\n", "- abstract rendering\n", "- grammar of graphics style plotting\n", "- ease of use\n" ] }, { "cell_type": "code", "collapsed": false, "input": [], "language": "python", "metadata": {}, "outputs": [] } ], "metadata": {} } ] }