In Python, Django is the most popular web framework. Second comes to Flask. What Flask is handy to do is to create some RESTful APIs by defining the web endpoints and the handlers. The frontend, however, is not something Django or Flask can provide to you. The best they can do is to ship a HTML and some JavaScript (also CSS) to the client side when a client loads the index page. Dash, however, is wrapping a default index page with some ReactJS stuff. So you can design your web in Python (i.e., server side code) and at the same time make some action handler. But this is all reactive, triggered from the client side. To make the server voluntarily push some data to the client, the standard way is to use WebSocket.
WebSocket is
not much different from a HTTP connection. It is a TCP socket with HTTP
handshake, and then upgraded to WebSocket. In the WebSocket stream, any kind
of data can be sent or received, including binary. The WebSocket URL starts
with ws://
rather than http://
. The SSL/TLS version is wss://
. On client
side, we should create a WebSocket
JavaScript object to use it and hoot some
event handler to the socket. But WebSocket is a fairly new thing. Legacy
browsers would not know that.
Since WebSocket protocol is not too much difference to a BSD socket or HTTP connection, it is possible to write a WebSocket handler in Python. There are a number of packages in PyPI for this. But some are not updated for long.
The one that I tried, found it works, and can work together with Flask and Dash is the Flask-SocketIO. It is not a plain WebSocket library, but a socket.io library. It is a protocol on top of WebSocket and with a JavaScript library for client side. Using it seems can save a few lines of code.
The way this set up works is the following: By default Dash is to run on Flask server, so as the Flask-SocketIO, as both are like “add-ons”. We set up Dash with a Flask server created. Then pass on this Flask server to SocketIO. Then normal things can be created as usual with the decorators: Flask routes, Dash callbacks, as well as SocketIO message handlers.
To run the server, it is important to run the SocketIO object. Dash already modified the Flask app for that it needs. But SocketIO app is different and more complicated to set up, which we can’t get it run other way round. Otherwise this is the error you will see:
RuntimeError: You need to use the gevent-websocket server. See the Deployment section of the documentation for more information.
Here is an example on how these work together. First is the server side Python script:
import random
import time
import threading
from flask_socketio import SocketIO
from dash import Dash, html, dcc
from dash.dependencies import Input, Output, State
external_scripts = [
{
"src": "https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.5.0/socket.io.js",
"integrity": "sha512-/xb5+PNOA079FJkngKI2jvID5lyiqdHXaUUcfmzE0X0BdpkgzIWHC59LOG90a2jDcOyRsd1luOr24UCCAG8NNw==",
"crossorigin": "anonymous",
},
"https://code.jquery.com/jquery-3.6.0.min.js",
"https://cdn.plot.ly/plotly-latest.min.js"
]
# the "servers"
app = Dash("brownian", external_scripts=external_scripts)
socketio = SocketIO(app.server, logger=True, engineio_logger=True)
# web layout, using Dash
app.title = "Brownian motion"
app.layout = html.Div(
id="root",
children=[
html.H1(
children="Brownian motion",
style={"textAlign": "center"}
),
html.Div(
className="flex-container",
children=[
html.Div(children=[
html.Div(id="showvariance"),
dcc.Slider(0, 2, 0.1, marks={0: "0", 0.5: "0.5", 1:"1", 1.5:"1.5", 2:"2"},
value=1, id="variance"),
]),
html.Div(
className="flex-container",
children=[
html.Div(id="pong"),
html.Button(id="reset", n_clicks=0, children="Reset", style={"margin":"20px", "padding":"10px"}),
html.Button(id="pause", n_clicks=0, children="Pause", style={"margin":"20px", "padding":"10px"}),
]
),
],
style={"textAlign": "center"}
),
dcc.Graph("motion"),
html.Div(id="dummy", hidden=True),
]
)
variance = 1
n = 0
paused = False
# Action for slider to adjust variance
@app.callback(Output(component_id="showvariance", component_property="children"),
Input("variance", "value"))
def update_variance(value):
global variance
variance = value
return f"Variance: {value}"
# Action for the "reset" button, client side callbacks
app.clientside_callback(
r"""function() {
n=0; x=[]; y=[];
var graph = document.getElementById("motion");
Plotly.newPlot(graph, data=[{x:x, y:y}]);
}""",
Output("dummy", "children"),
Input("reset", "n_clicks")
)
# Action for the "pause" button, client side callbacks
@app.callback(Output("pause", "children"),
Input("pause", "n_clicks"))
def toggle_pause(n_clicks):
global paused
paused = not paused
return "Resume" if paused else "Pause"
# websocket event handler
@socketio.on("ping")
def pong(n):
socketio.emit("pong", data=n)
# threading function, for websocket emit to all
def rng():
while True:
# Generate a random number once every 0.5 seconds
time.sleep(0.5)
if not paused:
socketio.emit("update", data=random.normalvariate(0,variance))
th = threading.Thread(target=rng, daemon=True)
th.start() # no th.join() as it it is a daemon thread that runs indefinitely
socketio.run(app.server, port=8050, debug=True)
# Won't work: app.run_server(debug=True)
This script deliberately demonstrate different styles of using Dash. We can
provide the external scripts as URL string or as dict. We need to launch two
servers, as variable app
(Dash) and socketio
(Flask-SocketIO), both hooked
to the same Flask instance, app.server
. Then we set up our page layout using
Dash widgets. An annoying fact of Dash is that all callback must go with an
output. Therefore we made a hidden “dummy” div element at the end (or we can
set style with {"display":"none"}
).
Some callbacks are easy: The slider just updates a global variable for the amount of variance. The reset button is to run some JavaScript on client side. The pause button toggles a boolean variable, which also updates the text on the button between “Resume” and “Pause”. These are just standard Dash things.
The key function is on the WebSocket side. This is a short piece of code we put
into assets/main.js
so Dash will send to the client when the page is loaded:
var socket = io();
var x = [];
var y = [];
var n = 0;
var timer = setInterval(ping, 1000);
function ping() {
socket.emit("ping", n++);
}
socket.on("pong", function(v) {
$("#pong").html(v.toString());
})
socket.on("update", function(z) {
x.push(n++);
if (y.length) {
y.push(y.at(-1) + z);
// before ES-2022: y.push(y[y.length-1] + z);
} else {
y.push(z)
};
var graph = document.getElementById("motion");
Plotly.newPlot(graph, data=[{x: x, y: y}]);
})
We set up two JavaScript arrays, x
and y
and push values into them on the
socket’s “update” event (this name is arbitrary). Then we refresh the graph
with the updated data. This “update” event is server side push. In the Python
code, we use the rng()
function that runs as a daemon thread to emit the data
(a floating point) to the client side once every 0.5 seconds.
At the same time, we set up a ping()
function that runs once a second on
JavaScript side that sends an integer n
using socket.io’s “ping” event to the
server side. The Python code’s pong()
function is set up as handler on this
event and echo back the integer on a “pong” event. The JavaScript side will
handle the “pong” event by updating a div element with the number it received.
This is a client side driven WebSocket communication. But the receive end on
client side is also set up as event handlers.
The Python script side must use socketio.run()
instead of Dash
app.run_server()
to properly set up the asynchronous framework for WebSocket.
For completeness, this is what I put into assets/main.css
to properly format the layout:
.flex-container {
display: flex;
padding: 5px;
flex-wrap: nowrap;
}
.flex-container > * {
flex-grow: 1
}
and the screen looks like this: