Skip to content

Commit 1ee386c

Browse files
committed
Add examples
support Bokeh 3 Buffers
1 parent 1a3b284 commit 1ee386c

File tree

14 files changed

+461
-3
lines changed

14 files changed

+461
-3
lines changed

bokeh_django/consumers.py

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,10 @@ async def handle(self, body: bytes) -> None:
154154
resources = self.resources(server_url) if resources_param != "none" else None
155155

156156
root_url = urljoin(absolute_url, self._prefix) if absolute_url else self._prefix
157-
bundle = bundle_for_objs_and_resources(None, resources) # , root_url=root_url) TODO add root_url argument in bokeh
157+
try:
158+
bundle = bundle_for_objs_and_resources(None, resources, root_url=root_url)
159+
except TypeError:
160+
bundle = bundle_for_objs_and_resources(None, resources)
158161

159162
render_items = [RenderItem(token=session.token, elementid=element_id, use_for_title=False)]
160163
bundle.add(Script(script_for_render_items({}, render_items, app_path=app_path, absolute_url=absolute_url)))
@@ -298,12 +301,21 @@ async def _send_bokeh_message(self, message: Message) -> int:
298301
await self.send(text_data=message.content_json)
299302
sent += len(message.content_json)
300303

301-
for header, payload in message._buffers:
304+
for buffer in message._buffers:
305+
if isinstance(buffer, tuple):
306+
header, payload = buffer
307+
else:
308+
# buffer is bokeh.core.serialization.Buffer (Bokeh 3)
309+
header = {'id': buffer.id}
310+
payload = buffer.data.tobytes()
311+
302312
await self.send(text_data=json.dumps(header))
303313
await self.send(bytes_data=payload)
304314
sent += len(header) + len(payload)
305-
except Exception: # Tornado 4.x may raise StreamClosedError
315+
316+
except Exception as e: # Tornado 4.x may raise StreamClosedError
306317
# on_close() is / will be called anyway
318+
log.exception(e)
307319
log.warning("Failed sending message as connection was closed")
308320
return sent
309321

examples/django_embed/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
/db.sqlite3

examples/django_embed/README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
Install django, panel and channels (pip only) and run:
2+
3+
```sh
4+
./manage.py runserver 0.0.0.0:8000
5+
```
6+
7+
then navigate to <http://127.0.0.1:8000/shape_viewer>, <http://127.0.0.1:8000/sea_surface>, or <http://127.0.0.1:8000/sea_surface_with_template>.
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
from bokeh.io import curdoc
2+
from bokeh.layouts import column
3+
from bokeh.models import ColumnDataSource, Slider
4+
from bokeh.plotting import figure
5+
from bokeh.sampledata.sea_surface_temperature import sea_surface_temperature
6+
7+
df = sea_surface_temperature.copy()
8+
source = ColumnDataSource(data=df)
9+
10+
plot = figure(x_axis_type="datetime", y_range=(0, 25), y_axis_label="Temperature (Celsius)",
11+
title="Sea Surface Temperature at 43.18, -70.43")
12+
plot.line("time", "temperature", source=source)
13+
14+
def callback(attr, old, new):
15+
if new == 0:
16+
data = df
17+
else:
18+
data = df.rolling(f"{new}D").mean()
19+
source.data = ColumnDataSource(data=data).data
20+
21+
slider = Slider(start=0, end=30, value=0, step=1, title="Smoothing by N Days")
22+
slider.on_change("value", callback)
23+
24+
doc = curdoc()
25+
doc.add_root(column(slider, plot))

examples/django_embed/django_embed/__init__.py

Whitespace-only changes.
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
"""
2+
ASGI config for django_embed project.
3+
4+
It exposes the ASGI callable as a module-level variable named ``application``.
5+
6+
For more information on this file, see
7+
https://channels.readthedocs.io/en/latest/deploying.html
8+
"""
9+
10+
import os
11+
12+
import django
13+
from channels.routing import get_default_application
14+
15+
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'django_embed.settings')
16+
django.setup()
17+
application = get_default_application()
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
from channels.auth import AuthMiddlewareStack
2+
from channels.routing import ProtocolTypeRouter, URLRouter
3+
from django.apps import apps
4+
5+
bokeh_app_config = apps.get_app_config('bokeh_django')
6+
7+
application = ProtocolTypeRouter({
8+
'websocket': AuthMiddlewareStack(URLRouter(bokeh_app_config.routes.get_websocket_urlpatterns())),
9+
'http': AuthMiddlewareStack(URLRouter(bokeh_app_config.routes.get_http_urlpatterns())),
10+
})
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
"""
2+
Django settings for django_embed project.
3+
4+
Generated by 'django-admin startproject' using Django 2.2.1.
5+
6+
For more information on this file, see
7+
https://docs.djangoproject.com/en/2.2/topics/settings/
8+
9+
For the full list of settings and their values, see
10+
https://docs.djangoproject.com/en/2.2/ref/settings/
11+
"""
12+
13+
from os.path import abspath, dirname, join
14+
from pathlib import Path
15+
16+
from bokeh.settings import bokehjsdir
17+
18+
# Build paths inside the project like this: join(BASE_DIR, ...)
19+
MODULE_DIR = dirname(abspath(__file__))
20+
BASE_DIR = dirname(MODULE_DIR)
21+
BASE_PATH = Path(BASE_DIR)
22+
23+
# Quick-start development settings - unsuitable for production
24+
# See https://docs.djangoproject.com/en/2.2/howto/deployment/checklist/
25+
26+
# SECURITY WARNING: keep the secret key used in production secret!
27+
SECRET_KEY = 'i^gyfuyz+zy66k%%hmy-kun(#n)2cj&)zry*+robiqh^x!x_&4'
28+
29+
# SECURITY WARNING: don't run with debug turned on in production!
30+
DEBUG = True
31+
32+
ALLOWED_HOSTS = []
33+
34+
35+
# Application definition
36+
37+
INSTALLED_APPS = [
38+
'django.contrib.admin',
39+
'django.contrib.auth',
40+
'django.contrib.contenttypes',
41+
'django.contrib.sessions',
42+
'django.contrib.messages',
43+
'channels',
44+
'bokeh_django',
45+
]
46+
47+
MIDDLEWARE = [
48+
'django.middleware.security.SecurityMiddleware',
49+
'django.contrib.sessions.middleware.SessionMiddleware',
50+
'django.middleware.common.CommonMiddleware',
51+
'django.middleware.csrf.CsrfViewMiddleware',
52+
'django.contrib.auth.middleware.AuthenticationMiddleware',
53+
'django.contrib.messages.middleware.MessageMiddleware',
54+
'django.middleware.clickjacking.XFrameOptionsMiddleware',
55+
]
56+
57+
ROOT_URLCONF = 'django_embed.urls'
58+
59+
TEMPLATES = [
60+
{
61+
'BACKEND': 'django.template.backends.django.DjangoTemplates',
62+
'DIRS': [join(MODULE_DIR, "templates")],
63+
'APP_DIRS': True,
64+
'OPTIONS': {
65+
'context_processors': [
66+
'django.template.context_processors.debug',
67+
'django.template.context_processors.request',
68+
'django.contrib.auth.context_processors.auth',
69+
'django.contrib.messages.context_processors.messages',
70+
],
71+
},
72+
},
73+
]
74+
75+
ASGI_APPLICATION = 'django_embed.routing.application'
76+
77+
78+
# Database
79+
# https://docs.djangoproject.com/en/2.2/ref/settings/#databases
80+
81+
DATABASES = {
82+
'default': {
83+
'ENGINE': 'django.db.backends.sqlite3',
84+
'NAME': join(BASE_DIR, 'db.sqlite3'),
85+
}
86+
}
87+
88+
89+
# Password validation
90+
# https://docs.djangoproject.com/en/2.2/ref/settings/#auth-password-validators
91+
92+
AUTH_PASSWORD_VALIDATORS = [
93+
{'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator'},
94+
{'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator'},
95+
{'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator'},
96+
{'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator'},
97+
]
98+
99+
100+
# Internationalization
101+
# https://docs.djangoproject.com/en/2.2/topics/i18n/
102+
103+
LANGUAGE_CODE = 'en-us'
104+
105+
TIME_ZONE = 'UTC'
106+
107+
USE_I18N = True
108+
109+
USE_L10N = True
110+
111+
USE_TZ = True
112+
113+
114+
# Static files (CSS, JavaScript, Images)
115+
# https://docs.djangoproject.com/en/2.2/howto/static-files/
116+
117+
STATIC_URL = '/static/'
118+
119+
STATICFILES_DIRS = [bokehjsdir()]
120+
121+
THEMES_DIR = join(MODULE_DIR, "themes")
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import numpy as np
2+
import panel as pn
3+
import param
4+
5+
from bokeh.plotting import figure
6+
7+
8+
class Shape(param.Parameterized):
9+
10+
radius = param.Number(default=1, bounds=(0, 1))
11+
12+
def __init__(self, **params) -> None:
13+
super().__init__(**params)
14+
self.figure = figure(x_range=(-1, 1), y_range=(-1, 1))
15+
self.renderer = self.figure.line(*self._get_coords())
16+
17+
def _get_coords(self):
18+
return [], []
19+
20+
def view(self):
21+
return self.figure
22+
23+
class Circle(Shape):
24+
25+
n = param.Integer(default=100, precedence=-1)
26+
27+
def __init__(self, **params) -> None:
28+
super().__init__(**params)
29+
30+
def _get_coords(self):
31+
angles = np.linspace(0, 2*np.pi, self.n+1)
32+
return (self.radius*np.sin(angles),
33+
self.radius*np.cos(angles))
34+
35+
@param.depends('radius', watch=True)
36+
def update(self):
37+
xs, ys = self._get_coords()
38+
self.renderer.data_source.data.update({'x': xs, 'y': ys})
39+
40+
class NGon(Circle):
41+
42+
n = param.Integer(default=3, bounds=(3, 10), precedence=1)
43+
44+
@param.depends('radius', 'n', watch=True)
45+
def update(self):
46+
xs, ys = self._get_coords()
47+
self.renderer.data_source.data.update({'x': xs, 'y': ys})
48+
49+
shapes = [NGon(), Circle()]
50+
51+
class ShapeViewer(param.Parameterized):
52+
53+
shape = param.ObjectSelector(default=shapes[0], objects=shapes)
54+
55+
@param.depends('shape')
56+
def view(self):
57+
return self.shape.view()
58+
59+
@param.depends('shape', 'shape.radius')
60+
def title(self):
61+
return '## %s (radius=%.1f)' % (type(self.shape).__name__, self.shape.radius)
62+
63+
def panel(self):
64+
expand_layout = pn.Column()
65+
66+
return pn.Column(
67+
pn.pane.HTML('<h1>Bokeh Integration Example using Param and Panel</h1>'),
68+
pn.widgets.Tabulator(),
69+
pn.Row(
70+
pn.Column(
71+
pn.panel(self.param, expand_button=False, expand=True, expand_layout=expand_layout),
72+
"#### Subobject parameters:",
73+
expand_layout),
74+
pn.Column(self.title, self.view)
75+
),
76+
sizing_mode='stretch_width',
77+
)
78+
79+
80+
def shape_viewer():
81+
return ShapeViewer().panel()
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<!doctype html>
2+
3+
<html lang="en">
4+
<head>
5+
<meta charset="utf-8">
6+
<title>Embedding a Bokeh Apps In Django via autoload.js</title>
7+
</head>
8+
9+
<body>
10+
<div>
11+
This Bokeh app below is served by a Django server (via autoload.js):
12+
</div>
13+
{{ script|safe }}
14+
</body>
15+
</html>
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
attrs:
2+
Plot:
3+
background_fill_color: "#DDDDDD"
4+
outline_line_color: white
5+
toolbar_location: above
6+
height: 500
7+
width: 800
8+
Grid:
9+
grid_line_dash: [6, 4]
10+
grid_line_color: white
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
"""django_embed URL Configuration
2+
3+
The `urlpatterns` list routes URLs to views. For more information please see:
4+
https://docs.djangoproject.com/en/2.2/topics/http/urls/
5+
Examples:
6+
Function views
7+
1. Add an import: from my_app import views
8+
2. Add a URL to urlpatterns: path('', views.home, name='home')
9+
Class-based views
10+
1. Add an import: from other_app.views import Home
11+
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
12+
Including another URLconf
13+
1. Import the include() function: from django.urls import include, path
14+
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
15+
"""
16+
from pathlib import Path
17+
18+
from django.apps import apps
19+
from django.conf import settings
20+
from django.contrib import admin
21+
from django.contrib.staticfiles.urls import staticfiles_urlpatterns
22+
from django.urls import path
23+
24+
import bokeh
25+
from bokeh_django import autoload, directory, document, static_extensions
26+
27+
from . import views
28+
29+
bokeh_app_config = apps.get_app_config('bokeh_django')
30+
31+
urlpatterns = [
32+
path("admin/", admin.site.urls),
33+
path("sea_surface/", views.sea_surface),
34+
path("my_sea_surface/", views.sea_surface_custom_uri),
35+
path("shapes", views.shapes)
36+
]
37+
38+
base_path = settings.BASE_PATH
39+
40+
bokeh_apps = [
41+
autoload("sea_surface", views.sea_surface_handler),
42+
autoload('shapes', views.shape_viewer_handler),
43+
document("sea_surface_with_template", views.sea_surface_handler_with_template),
44+
document("bokeh_apps/sea_surface", base_path / "bokeh_apps" / "sea_surface.py"),
45+
document("shape_viewer", views.shape_viewer_handler),
46+
]
47+
48+
apps_path = Path(bokeh.__file__).parent.parent / "examples" / "app"
49+
bokeh_apps += directory(apps_path)
50+
51+
urlpatterns += static_extensions()
52+
urlpatterns += staticfiles_urlpatterns()

0 commit comments

Comments
 (0)