Running Panel apps inside Django#

Panel generally runs on the Bokeh server which itself runs on Tornado. However, it is also often useful to embed a Panel app in large web application, such as a Django web server. Using Panel with Django requires a bit more work than for notebooks and Bokeh servers.

To run this example app yourself, you will first need to install django (e.g. conda install "django=2").

Additionally, you should also install the channels library (using pip install channels==2 or conda install channels=2 -c conda-forge). This makes it possible to run bokeh without launching a separate Tornado server.

Note that these examples can also be run with django 3 or django 4 (which will require channels 3), by installing the additional bokeh_django library. In this case replace all instances of bokeh.server.django with bokeh_django in the example below.

Configuration#

Before we start adding a bokeh app to our Django server we have to set up some of the basic plumbing. In the examples/apps/django/project folder we will add some basic configurations.

First of all we need to set up a Asynchronous Server Gateway Interface (ASGI) instead of the usual WSGI setup. For this purpose we add examples/apps/django/project/asgi.py:

import os

import django

from channels.routing import get_default_application

os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'project.settings')

django.setup()

application = get_default_application()

Next we need to ensure the routing is configured correctly to handle a bokeh server in examples/apps/django/project/routing.py:

from channels.auth import AuthMiddlewareStack
from channels.routing import ProtocolTypeRouter, URLRouter
from django.apps import apps

bokeh_app_config = apps.get_app_config('bokeh.server.django')

application = ProtocolTypeRouter({
    'websocket': AuthMiddlewareStack(URLRouter(bokeh_app_config.routes.get_websocket_urlpatterns())),
    'http': AuthMiddlewareStack(URLRouter(bokeh_app_config.routes.get_http_urlpatterns())),
})

Lastly we need to add some configuration to examples/apps/django/project/settings.py. As a first step we need to add both channels and bokeh.server.django to the INSTALLED_APPS:


INSTALLED_APPS = [
    ...,
    'channels',
    'bokeh.server.django',
]

Secondly we need to declare the bokehjsdir as part of the STATICFILES_DIRS:

from bokeh.settings import bokehjsdir

STATICFILES_DIRS = [bokehjsdir()]

Now we need to add any templates we have:

TEMPLATES = [
    {
        'DIRS': [os.path.join(BASE_DIR, 'sliders', 'templates')],
        ...: ...,
    }
]

and lastly add the app(s) and static_extensions() to the urlpatterns in the urls.py file:

from bokeh.server.django import autoload, static_extensions
from django.apps import apps
from django.contrib import admin
from django.urls import path, include
from django.contrib.staticfiles.urls import staticfiles_urlpatterns

import sliders.pn_app as sliders_app

pn_app_config = apps.get_app_config('bokeh.server.django')

urlpatterns = [
    path('sliders/', include('sliders.urls')),
    path('admin/', admin.site.urls),
]

bokeh_apps = [
    autoload("sliders", sliders_app.app),
]

urlpatterns += static_extensions()
urlpatterns += staticfiles_urlpatterns()

Now it’s time to configure an actual app and add it to our Django server.

Sliders app#

Based on a standard Django app template, this app shows how to integrate Panel with a Django view

The sliders app is in examples/apps/django/sliders. We will cover the following additions/modifications to the Django app template:

  • sliders/sinewave.py: a parameterized object (representing your pre-existing code)

  • sliders/pn_app.py: creates an app function from the SineWave class

  • sliders/apps.py: how a Django app can import and use Bokeh server

  • sliders/views.py and templates/base.html: getting the Bokeh app into a Django view

screenshot of sliders app

To start with, in sliders/sinewave.py we create a parameterized object to serve as a placeholder for your own, existing code:

import numpy as np
import param
from bokeh.models import ColumnDataSource
from bokeh.plotting import figure


class SineWave(param.Parameterized):
    offset = param.Number(default=0.0, bounds=(-5.0, 5.0))
    amplitude = param.Number(default=1.0, bounds=(-5.0, 5.0))
    phase = param.Number(default=0.0, bounds=(0.0, 2 * np.pi))
    frequency = param.Number(default=1.0, bounds=(0.1, 5.1))
    N = param.Integer(default=200, bounds=(0, None))
    x_range = param.Range(default=(0, 4 * np.pi), bounds=(0, 4 * np.pi))
    y_range = param.Range(default=(-2.5, 2.5), bounds=(-10, 10))

    def __init__(self, **params):
        super(SineWave, self).__init__(**params)
        x, y = self.sine()
        self.cds = ColumnDataSource(data=dict(x=x, y=y))
        self.plot = figure(plot_height=400, plot_width=400,
                           tools="crosshair, pan, reset, save, wheel_zoom",
                           x_range=self.x_range, y_range=self.y_range)
        self.plot.line('x', 'y', source=self.cds, line_width=3, line_alpha=0.6)

    @param.depends('N', 'frequency', 'amplitude', 'offset', 'phase', 'x_range', 'y_range', watch=True)
    def update_plot(self):
        x, y = self.sine()
        self.cds.data = dict(x=x, y=y)
        self.plot.x_range.start, self.plot.x_range.end = self.x_range
        self.plot.y_range.start, self.plot.y_range.end = self.y_range

    def sine(self):
        x = np.linspace(0, 4 * np.pi, self.N)
        y = self.amplitude * np.sin(self.frequency * x + self.phase) + self.offset
        return x, y

However the app itself is defined we need to configure an entry point, which is a function that accepts a bokeh Document and adds the application to it. In case of the slider app it looks like this:

import panel as pn

from .sinewave import SineWave

def app(doc):
    sw = SineWave()
    row = pn.Row(sw.param, sw.plot)
    row.server_doc(doc)

Next we create a views.py file which returns a view the Django server can render:

# Create your views here.
from bokeh.embed import server_document

from django.http import HttpRequest, HttpResponse
from django.shortcuts import render


def sliders(request: HttpRequest) -> HttpResponse:
    script = server_document(request.build_absolute_uri())
    return render(request, "base.html", dict(script=script))

The base.html template should be in the TEMPLATES DIRS directory we declared in the settings.py file above. A very basic template might look like this but can be as complex as you need:

<!DOCTYPE html>
<html>
  <head>
    <title>Panel in Django: sliders</title>
  </head>
  <body>
  {% block content %}
    {{ script|safe }}
  {% endblock %}
  </body>
</html>

Next we declare a urls.py file to declare the urlpattern where to serve the sliders app to Django:

from django.urls import path

from . import views

app_name = 'sliders'
urlpatterns = [
    path('', views.sliders, name='sliders'),
]

You should be able to run this app yourself by changing to the examples/apps/django directory and then running: python manage.py runserver; then visit http://localhost:8000/sliders in your browser to try the app.

Multiple apps#

This is the most basic configuration for a bokeh server. It is of course possible to add multiple apps in the same way and then registering them with Django in the way described in the configuration section above. To see a multi-app Django server have a look at examples/apps/django_multi_apps and launch it with python manage.py runserver as before.