{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "\n", "\n", "## Clifford attractors with Datashader, Panel, and interact\n", "\n", "[Clifford attractors](attractors.ipynb) are a type of iterative equation that traces the path of a particle through a 2D space using functions of sine and cosine terms that make interesting \"attractor\" patterns (covering only some portions of the possible space, in certain shapes). \n", "\n", "Here we use Numpy and Pandas to calculate a dataframe consisting of millions of such locations, using [Numba](numba.pydata.org) to make generating them 50X faster than bare Python. We'll then plot the results as a static image using [Datashader](http://datashader.org), which renders arbitrarily large data into fixed-sized images. \n", "\n", "If you can have a live Python process running (not just a static webpage or anaconda.org notebook viewer), we'll also show you how to make an interactive app for exploring parameter values, and a standalone dashboard suitable for sharing widely. Before you run the notebook or server, you'll need to set up a [conda](http://conda.pydata.org/miniconda.html) environment and run `conda install -c pyviz datashader panel`." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import numpy as np, pandas as pd\n", "from numba import jit\n", "\n", "@jit(nopython=True)\n", "def clifford_trajectory(a, b, c, d, x0, y0, n):\n", " xs, ys = np.zeros(n), np.zeros(n)\n", " xs[0], ys[0] = x0, y0\n", " for i in np.arange(n-1):\n", " xs[i+1] = np.sin(a * ys[i]) + c * np.cos(a * xs[i])\n", " ys[i+1] = np.sin(b * xs[i]) + d * np.cos(b * ys[i])\n", " return xs, ys" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We can visualize the resulting dataframe using [Datashader](http://datashader.org), with colormaps from [Colorcet](http://colorcet.pyviz.org):" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import datashader as ds\n", "from datashader import transfer_functions as tf\n", "\n", "from colorcet import palette_n\n", "ps ={k:p[::-1] for k,p in palette_n.items()}\n", "\n", "import panel as pn\n", "pn.extension()\n", "\n", "def clifford_plot(a=1.9, b=1.9, c=1.9, d=0.8, n=1000000, colormap=ps['kbc']):\n", " cvs = ds.Canvas(plot_width=600, plot_height=600)\n", " xs, ys = clifford_trajectory(a, b, c, d, 0, 0, n)\n", " agg = cvs.points(pd.DataFrame({'x':xs, 'y':ys}), 'x', 'y')\n", " return tf.shade(agg, cmap=colormap)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "clifford_plot(a=1.7, b=1.7, c=0.6, d=1.2, n=20000000, colormap=ps['dimgray'])" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Twenty million data points from an attractor clearly makes an interesting shape! The shapes depend on the parameters provided, and we can now easily build a control panel for exploring the effect of those parameters, using interactive widgets from [Panel](http://panel.pyviz.org):" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "pn.interact(clifford_plot, n=(1,20000000), colormap=ps)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Here `interact()` returns an object that lets us vary the arguments to the given function interactively and see the results, as long as this notebook is backed by a live, running Python process. \n", "\n", "`interact()` is great for quick exploration, but it's not a dead end -- what it returns is actually a reusable, compositional `Panel` object, and we can use its components to build a more customized dashboard if we like. Let's define an object `i` to capture the result of `interact`, and see what we've got:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "i = pn.interact(clifford_plot, a=(0,2), b=(0,2), c=(0,2), d=(0,2), \n", " n=(1,20000000), colormap=ps, panel_layout=pn.Row)\n", "print(i)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "As you can see, the resulting object is a `pn.Column` of two items: a `pn.WidgetBox` at index 0 and `pn.interactive` object at index 1. We can now mix and match these components into any dashboard configuration we like. For instance, if the only widget we wanted was for the colormap, we could make a special-purpose app just for that using `pn.Column(i[0][5], i[1])` (returning a column with just the \"Select\" widget above the image output). \n", "\n", "Here let's remix these components into a new app that can be served as a standalone dashboard. We'll put the widgets into a column with a logo image and some Markdown text to explain how to use the dashboard, and put that column into a row with the image output:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "logo = \"https://tinyurl.com/y9c2zn65/logo_stacked_s.png\"\n", "\n", "text = pn.panel(\"#### Use the widgets to vary the parameters of this \"\n", " \"[Clifford attractor](attractors.ipynb).\\n\\n\"\n", " \"#### Note that many values result in nearly \"\n", " \"blank plots that contain only a few scattered points.\", \n", " width=310, height=100)\n", "\n", "pn.Row(pn.Column(logo, text, i[0]), i[1]).servable();" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We could have displayed the new object here in the notebook by removing the semicolon at the end of the cell, but this expanded dashboard version isn't actually that useful in a notebook, where we already have ways of showing logos and text. Instead, the expanded version is meant for launching as a standalone server outside of the notebook, and so we've marked that object `.servable()` to declare that if someone later runs this notebook as a server process (using `panel serve --show CliffordInteract.ipynb`), your browser will open a separate window with the serveable object ready to explore or share, just like the screenshot at the top of this notebook. So it's your choice -- use a static Image in a Jupyter cell, make it an interactive app using Panel's `interact`, or very easily reconfigure it into a shareable, deployable dashboard!" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "#! panel serve --show --port 5009 clifford_panel.ipynb" ] } ], "metadata": { "language_info": { "name": "python", "pygments_lexer": "ipython3" } }, "nbformat": 4, "nbformat_minor": 2 }