Skip to content

Commit c2bdc18

Browse files
Merge pull request #29 from delsim/doc_live_updating
Documentation for live updating
2 parents b94e432 + 91a1530 commit c2bdc18

File tree

14 files changed

+223
-23
lines changed

14 files changed

+223
-23
lines changed

demo/demo/plotly_apps.py

Lines changed: 29 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,7 @@ def callback_c(*args, **kwargs):
134134
html.Div(id='button_local_counter', children="Press any button to start"),
135135
], className="")
136136

137+
#pylint: disable=too-many-arguments
137138
@liveIn.expanded_callback(
138139
dash.dependencies.Output('button_local_counter', 'children'),
139140
[dash.dependencies.Input('red-button', 'n_clicks'),
@@ -186,10 +187,10 @@ def callback_liveIn_button_press(red_clicks, blue_clicks, green_clicks,
186187
datetime.fromtimestamp(0.001*timestamp))
187188

188189
liveOut = DjangoDash("LiveOutput",
189-
)#serve_locally=True)
190+
)#serve_locally=True)
190191

191192
def _get_cache_key(state_uid):
192-
return "demo-liveout-s4-%s" % state_uid
193+
return "demo-liveout-s6-%s" % state_uid
193194

194195
def generate_liveOut_layout():
195196
'Generate the layout per-app, generating each tine a new uuid for the state_uid argument'
@@ -210,6 +211,7 @@ def generate_liveOut_layout():
210211

211212
liveOut.layout = generate_liveOut_layout
212213

214+
#pylint: disable=unused-argument
213215
#@liveOut.expanded_callback(
214216
@liveOut.callback(
215217
dash.dependencies.Output('internal_state', 'children'),
@@ -242,6 +244,18 @@ def callback_liveOut_pipe_in(named_count, state_uid, **kwargs):
242244
colour_set = [(None, 0, 100) for i in range(5)]
243245

244246
_, last_ts, prev = colour_set[-1]
247+
248+
# Loop over all existing timestamps and find the latest one
249+
if not click_timestamp or click_timestamp < 1:
250+
click_timestamp = 0
251+
252+
for _, the_colour_set in state.items():
253+
_, lts, _ = the_colour_set[-1]
254+
if lts > click_timestamp:
255+
click_timestamp = lts
256+
257+
click_timestamp = click_timestamp + 1000
258+
245259
if click_timestamp > last_ts:
246260
colour_set.append((user, click_timestamp, prev * random.lognormvariate(0.0, 0.1)),)
247261
colour_set = colour_set[-100:]
@@ -268,23 +282,28 @@ def callback_show_timeseries(internal_state_string, state_uid, **kwargs):
268282

269283
colour_series = {}
270284

285+
colors = {'red':'#FF0000',
286+
'blue':'#0000FF',
287+
'green':'#00FF00',
288+
'yellow': '#FFFF00',
289+
'cyan': '#00FFFF',
290+
'magenta': '#FF00FF',
291+
'black' : '#000000',
292+
}
293+
271294
for colour, values in state.items():
272295
timestamps = [datetime.fromtimestamp(int(0.001*ts)) for _, ts, _ in values if ts > 0]
273-
users = [user for user, ts, _ in values if ts > 0]
296+
#users = [user for user, ts, _ in values if ts > 0]
274297
levels = [level for _, ts, level in values if ts > 0]
275-
colour_series[colour] = pd.Series(levels, index=timestamps).groupby(level=0).first()
298+
if colour in colors:
299+
colour_series[colour] = pd.Series(levels, index=timestamps).groupby(level=0).first()
276300

277301
df = pd.DataFrame(colour_series).fillna(method="ffill").reset_index()[-25:]
278302

279-
colors = {'red':'#FF0000',
280-
'blue':'#0000FF',
281-
'green':'#00FF00',
282-
}
283-
284303
traces = [go.Scatter(y=df[colour],
285304
x=df['index'],
286305
name=colour,
287-
line=dict(color=colors[colour]),
306+
line=dict(color=colors.get(colour, '#000000')),
288307
) for colour in colour_series]
289308

290309
return {'data':traces,

demo/demo/settings.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,8 @@
125125
"ws_route" : "ws/channel",
126126

127127
"insert_demo_migrations" : True, # Insert model instances used by the demo
128+
129+
"http_poke_enabled" : True, # Flag controlling availability of direct-to-messaging http endpoint
128130
}
129131

130132
# Static files (CSS, JavaScript, Images)
@@ -165,6 +167,10 @@
165167
# can be useful for development especially if offline - we add in the root directory
166168
# of each module. This is a bit of fudge and only needed if serve_locally=True is
167169
# set on a DjangoDash instance.
170+
#
171+
# Note that this makes all of the python module (including .py and .pyc) files available
172+
# through the static route. This is not a big deal for development but at the same time
173+
# not particularly neat or tidy.
168174

169175
if DEBUG:
170176

demo/demo/templates/base.html

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@
2525
<a class="nav-item nav-link btn btn-lg" href="{%url "demo-two"%}">Demo Two - Initial State</a>
2626
<a class="nav-item nav-link btn btn-lg" href="{%url "demo-three"%}">Demo Three - Enhanced Callbacks</a>
2727
<a class="nav-item nav-link btn btn-lg" href="{%url "demo-four"%}">Demo Four - Live Updating</a>
28+
<a class="nav-item nav-link btn btn-lg"
29+
target="_blank"
30+
href="https://django-plotly-dash.readthedocs.io/en/latest/">Online Documentation</a>
2831
{%endblock%}
2932
</div>
3033
</nav>

demo/demo/templates/demo_four.html

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,4 +41,17 @@ <h1>Live Updating</h1>
4141
{%plotly_app slug="liveoutput-2" ratio=0.5 %}
4242
</div>
4343
</div>
44+
<p>
45+
</p>
46+
<p>
47+
Any http command
48+
can be used to send a message to the apps. This is equiavent to a press of
49+
the red button. Other colours can be specified, including yellow, cyan and black in
50+
addition to the three named in the LiveInput app.
51+
</p>
52+
<div class="card bg-light border-dark">
53+
<div class="card-body">
54+
curl http://localhost:8000/dpd/views/poke/ -d'{"channel_name":"live_button_counter","label":"named_counts","value":{"click_colour":"red"}}'
55+
</div>
56+
</div>
4457
{%endblock%}

demo/demo/urls.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@
2828
# Load demo plotly apps - this triggers their registration
2929
import demo.plotly_apps # pylint: disable=unused-import
3030

31+
from django_plotly_dash.views import add_to_session
32+
3133
urlpatterns = [
3234
url('^$', TemplateView.as_view(template_name='index.html'), name="home"),
3335
url('^demo-one$', TemplateView.as_view(template_name='demo_one.html'), name="demo-one"),
@@ -36,6 +38,8 @@
3638
url('^demo-four$', TemplateView.as_view(template_name='demo_four.html'), name="demo-four"),
3739
url('^admin/', admin.site.urls),
3840
url('^django_plotly_dash/', include('django_plotly_dash.urls')),
41+
42+
url('^demo-session-var$', add_to_session, name="session-variable-example"),
3943
]
4044

4145
# Add in static routes so daphne can serve files; these should

django_plotly_dash/routing.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,19 @@
2929
from django.conf.urls import url
3030

3131
from .consumers import MessageConsumer, PokePipeConsumer
32-
from .util import pipe_ws_endpoint_name, http_endpoint
32+
from .util import pipe_ws_endpoint_name, http_endpoint, http_poke_endpoint_enabled
3333

3434
# TODO document this and discuss embedding with other routes
35+
36+
http_routes = [
37+
]
38+
39+
if http_poke_endpoint_enabled():
40+
http_routes.append(url(http_endpoint("poke"), PokePipeConsumer))
41+
42+
http_routes.append(url("^", AsgiHandler)) # AsgiHandler is 'the normal Django view handlers'
43+
3544
application = ProtocolTypeRouter({
3645
'websocket': AuthMiddlewareStack(URLRouter([url(pipe_ws_endpoint_name(), MessageConsumer),])),
37-
'http': AuthMiddlewareStack(URLRouter([url(http_endpoint("poke"), PokePipeConsumer),
38-
url("^", AsgiHandler),])), # AsgiHandler is 'the normal Django view handlers'
46+
'http': AuthMiddlewareStack(URLRouter(http_routes)),
3947
})

django_plotly_dash/util.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,3 +49,7 @@ def insert_demo_migrations():
4949
'Check settings and report if objects for demo purposes should be inserted during migration'
5050

5151
return _get_settings().get('insert_demo_migrations', False)
52+
53+
def http_poke_endpoint_enabled():
54+
'Return true if the http endpoint is enabled through the settings'
55+
return _get_settings().get('http_poke_enabled', True)

django_plotly_dash/views.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,3 +106,18 @@ def component_suites(request, resource=None, component=None, **kwargs):
106106
redone_url = "/static/dash/%s/%s" %(component, resource)
107107

108108
return HttpResponseRedirect(redirect_to=redone_url)
109+
110+
111+
# pylint: disable=wrong-import-position, wrong-import-order
112+
from django.template.response import TemplateResponse
113+
114+
def add_to_session(request, template_name="index.html", **kwargs):
115+
'Add some info to a session in a place that django-plotly-dash can pass to a callback'
116+
117+
django_plotly_dash = request.session.get("django_plotly_dash", dict())
118+
119+
session_add_count = django_plotly_dash.get('add_counter', 0)
120+
django_plotly_dash['add_counter'] = session_add_count + 1
121+
request.session['django_plotly_dash'] = django_plotly_dash
122+
123+
return TemplateResponse(request, template_name, {})

docs/configuration.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ below.
1616
# Route used for direct http insertion of pipe messages
1717
"http_route" : "dpd/views",
1818
19+
# Flag controlling existince of http poke endpoint
20+
"http_poke_enabled" : True,
21+
1922
# Insert data for the demo when migrating
2023
"insert_demo_migrations" : False,
2124
}

docs/dash_components.rst

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,39 @@
33
Dash components
44
===============
55

6-
The ``dpd-components`` package contains ``Dash`` components.
6+
The ``dpd-components`` package contains ``Dash`` components. This package is installed as a
7+
dependency of ``django-plotly-dash``.
78

89
.. _pipe_component:
910
The ``Pipe`` component
1011
--------------
1112

12-
Blah
13+
Each ``Pipe`` component instance listens for messages on a single channel. The ``value`` member of any message on that channel whose ``label`` matches
14+
that of the component will be used to update the ``value`` property of the component. This property can then be used in callbacks like
15+
any other ``Dash`` component property.
16+
17+
An example, from the demo application:
18+
19+
.. code-block:: python
20+
21+
import dpd_components as dpd
22+
23+
app.layout = html.Div([
24+
...
25+
dpd.Pipe(id="named_count_pipe", # ID in callback
26+
value=None, # Initial value prior to any message
27+
label="named_counts", # Label used to identify relevant messages
28+
channel_name="live_button_counter"), # Channel whose messages are to be examined
29+
...
30+
])
31+
32+
The ``value`` of the message is sent from the server to all front ends with ``Pipe`` components listening
33+
on the given ``channel_name``. This means that this part of the message should be small, and it must
34+
be JSON serialisable. Also, there is no guarantee that any callbacks will be executed in the same Python
35+
process as the one that initiated the initial message from server to front end.
36+
37+
The ``Pipe`` properties can be persisted like any other ``DashApp`` instance, although it is unlikely
38+
that continued persistence of state on each update of this component is likely to be useful.
39+
40+
This component requires a bidirectional connection, such as a websocket, to the server. Inserting
41+
a ``plotly_message_pipe`` :ref:`template tag <plotly_message_pipe>` is sufficient.

0 commit comments

Comments
 (0)