Bowtie: Interactive Dashboard Toolkit¶
Bowtie helps you visualize your data interactively. No Javascript required, you build your dashboard in pure Python. Easy to deploy so you can share results with others.
New to Bowtie? The quickstart guide will get you running your first app. It takes about 10 minutes to go through.
Notable Features¶
- Ships with many useful widgets including charts, tables, dropdown menus, sliders, and markdown.
- All widgets come equipped with events and commands for interaction.
- Hook into Plotly charts with click, selection, and hover events.
- Jupyter integration allows you to prototype your dashboards.
- Schedule functions to run on a timer.
- Store and fetch data with the client (browser).
- Built in progress indicators for all visual widgets.
- Powerful Pythonic grid API to layout components, not in HTML and CSS.
- Compiles a single Javascript bundle speeding up load times and removes CDN dependencies.
- Powerful programming model let’s you listen to multiple events and update multiple widgets with single functions.
Contents¶
Quick Start¶
This quick start will show how to do the following:
- Install everything needed to use Bowtie.
- Write an app connecting a slider to a plot.
- How to deploy to Heroku.
Install Bowtie¶
If you use conda
, you can install with:
conda install -c conda-forge bowtie-py
If you use pip
, you can install with:
pip install bowtie
To install bleeding edge you can use flit, to install:
flit install
Install Yarn¶
Bowtie uses Yarn to manage the Javascript libraries
We need to install it before we can use Bowtie.
If you installed Bowtie with conda
, Yarn was installed as a dependency and you can move on to Creating Your First App.
Other Environments¶
For other environments please follow the install instructions on the official website.
Creating Your First App¶
We will be creating a slider that controls the frequency of a sinusoid and visualizing the sine wave.
First we’ll import the App
class and two components we will use:
from bowtie import App
from bowtie.visual import Plotly
from bowtie.control import Slider
import numpy as np
I imported Numpy to generate sine waves.
Now we instantiate the App
and the components and configure them:
app = App(sidebar=True)
chart = Plotly()
slider = Slider(minimum=1, maximum=10, start=5, step=0.1)
Next we add these components to the app
so they will be displayed on the web page.
We place the slider in the sidebar and place the sine chart in the main view:
app.add_sidebar(slider)
app.add(chart)
Next we’ll create a listener that generates a plot on slider changes:
@app.subscribe(slider.on_change)
def plot_sine(freq):
t = np.linspace(0, 10, 100)
chart.do_all({
'data': [{
'type': 'scatter',
'mode': 'lines+markers',
'x': t,
'y': np.sin(freq * t)
}]
})
The bowtie.control.Slider
component sends its values as a list of strings so we had to cast it to a float.
Lastly we need to build the application by laying out the components and connecting listeners to events.
The App
class handles this and we put this logic into a function.
Bowtie provides a decorator, command
, which we’ll use to make a simple command line interface.
To finish, we simply wrap the function with the command
decorator:
from bowtie import command
@command
def main():
return app
Now take a look at the CLI we just created by running this script:
python app.py
The output should look something like this:
Usage: app.py [--help] COMMAND [ARGS]...
Bowtie CLI to help build and run your app.
Options:
--help Show this message and exit.
Commands:
build Writes the app, downloads the packages, and...
dev Recompiles the app for development.
prod Recompiles the app for production.
run Build the app and serve it.
serve Serves the Bowtie app locally.
To construct the app, we run the script with the build
command:
python app.py build
This will construct the app, install the JavaScript libraries and compile your project. Once it’s done you should be able to run the following to launch your app:
python app.py serve
That will launch the app locally and you should be able to access it at http://localhost:9991.
Deploy to Heroku¶
This isn’t well documented, but you can try the following. For example, this was done to create bowtie-demo so you may refer to that.
Create the Procfile, try the following:
web: python app.py serve -p $PORT
Create requirements files, again see bowtie-demo for an example.
Rebuild with production settings with webpack, by default Bowtie makes a development build:
python app.py prod
We need to add the Javascript, so commit the following file:
git add build/bundle.js.gz
Finally push your repo to Heroku!:
git push heroku master
App¶
The App class defines the structure of the application.
It contains a root view, what you get when you go to /
.
It subscribes functions to component events and builds the application.
-
class
bowtie.
App
(name='__main__', app=None, rows: int = 1, columns: int = 1, sidebar: bool = False, title: str = 'Bowtie App', theme: Optional[str] = None, background_color: str = 'White', socketio: str = '', debug: bool = False)[source]¶ Core class to layout, connect, build a Bowtie app.
Create a Bowtie App.
Parameters: - name (str, optional) – Use __name__ or leave as default if using a single module. Consult the Flask docs on “import_name” for details on more complex apps.
- app (Flask app, optional) – If you are defining your own Flask app, pass it in here. You only need this if you are doing other stuff with Flask outside of bowtie.
- row (int, optional) – Number of rows in the grid.
- columns (int, optional) – Number of columns in the grid.
- sidebar (bool, optional) – Enable a sidebar for control components.
- title (str, optional) – Title of the HTML.
- theme (str, optional) – Color for Ant Design components.
- background_color (str, optional) – Background color of the control pane.
- socketio (string, optional) – Socket.io path prefix, only change this for advanced deployments.
- debug (bool, optional) – Enable debugging in Flask. Disable in production!
-
add
(component: bowtie._component.Component) → None[source]¶ Add a widget to the grid in the next available cell.
Searches over columns then rows for available cells.
Parameters: component (bowtie._Component) – A Bowtie component instance.
-
add_route
(view: bowtie._app.View, path: str, exact: bool = True) → None[source]¶ Add a view to the app.
Parameters:
Add a widget to the sidebar.
Parameters: widget (bowtie._Component) – Add this widget to the sidebar, it will be appended to the end.
-
load
(func: Callable) → Callable[source]¶ Call a function on page load.
Parameters: func (callable) – Function to be called.
-
schedule
(seconds: float)[source]¶ Call a function periodically.
Parameters: - seconds (float) – Minimum interval of function calls.
- func (callable) – Function to be called.
-
subscribe
(*events) → Callable[source]¶ Call a function in response to an event.
If more than one event is given, func will be given as many arguments as there are events.
If the pager calls notify, the decorated function will be called.
Parameters: *event (event or pager) – Bowtie event, must have at least one. Examples
Subscribing a function to multiple events.
>>> from bowtie.control import Dropdown, Slider >>> app = App() >>> dd = Dropdown() >>> slide = Slider() >>> @app.subscribe(dd.on_change, slide.on_change) ... def callback(dd_item, slide_value): ... pass >>> @app.subscribe(dd.on_change) ... @app.subscribe(slide.on_change) ... def callback2(value): ... pass
Using the pager to run a callback function.
>>> from bowtie.pager import Pager >>> app = App() >>> pager = Pager() >>> @app.subscribe(pager) ... def callback(): ... pass >>> def scheduledtask(): ... pager.notify()
View¶
Views are responsible for laying components out on a webpage. Each view defines a grid and optional sidebar. Each app comes with one root view and you can add as many additional routes and view as you want.
-
class
bowtie.
View
(rows: int = 1, columns: int = 1, sidebar: bool = False, background_color: str = 'White')[source]¶ Grid of components.
Create a new grid.
Parameters: -
add
(component: Union[bowtie._component.Component, Sequence[bowtie._component.Component]]) → None[source]¶ Add a widget to the grid in the next available cell.
Searches over columns then rows for available cells.
Parameters: components (bowtie._Component) – A Bowtie widget instance.
Add a widget to the sidebar.
Parameters: component (bowtie._Component) – Add this component to the sidebar, it will be appended to the end.
-
Size¶
Each row and column in the app is a Size
object.
The space used by each row and column can be changed through the following methods.
-
class
bowtie._app.
Size
[source]¶ Size of rows and columns in grid.
This is accessed through
.rows
and.columns
from App and View instances.This uses CSS’s minmax function.
The minmax() CSS function defines a size range greater than or equal to min and less than or equal to max. If max < min, then max is ignored and minmax(min,max) is treated as min. As a maximum, a <flex> value sets the flex factor of a grid track; it is invalid as a minimum.
Examples
Laying out an app with the first row using 1/3 of the space and the second row using 2/3 of the space.
>>> app = App(rows=2, columns=3) >>> app.rows[0].fraction(1) 1fr >>> app.rows[1].fraction(2) 2fr
Create a default row or column size with fraction = 1.
Gap¶
Set the margin between the cells of the grid in the App.
Components¶
Find the list of supported components here and how to use them.
Visuals¶
Plotly¶
-
class
bowtie.visual.
Plotly
(init: Optional[Dict] = None)[source]¶ Plotly component.
Useful for many kinds of plots.
Create a Plotly component.
Parameters: init (dict, optional) – Initial Plotly data to plot. -
do_all
(plot)[source]¶ Replace the entire plot.
Parameters: plot (dict) – Dict that can be plotted with Plotly. It should have this structure: {data: [], layout: {}}
.Returns: Return type: None
-
do_config
(config)[source]¶ Update the configuration of the plot.
Parameters: config (dict) – Plotly config information. Returns: Return type: None
-
do_data
(data)[source]¶ Replace the data portion of the plot.
Parameters: data (list of traces) – List of data to replace the old data. Returns: Return type: None
-
do_layout
(layout)[source]¶ Update the layout.
Parameters: layout (dict) – Contains layout information. Returns: Return type: None
-
on_beforehover
¶ Emit an event before hovering over a point.
Payload: TODO.Returns: Name of event. Return type: str
-
on_hover
¶ Emit an event after hovering over a point.
Payload: TODO.Returns: Name of event. Return type: str
-
on_relayout
¶ Emit an event when the chart axes change.
Payload: TODO.Returns: Name of event. Return type: str
-
Table¶
-
class
bowtie.visual.
Table
(data=None, columns: Optional[List[Union[int, str]]] = None, results_per_page: int = 10)[source]¶ Ant Design table with filtering and sorting.
Create a table and optionally initialize the data.
Parameters:
SVG¶
-
class
bowtie.visual.
SVG
(preserve_aspect_ratio: bool = False)[source]¶ SVG image.
Mainly for matplotlib plots.
Create SVG component.
Parameters: preserve_aspect_ratio (bool, optional) – If True
it preserves the aspect ratio otherwise it will stretch to fill up the space available.-
do_image
(image)[source]¶ Replace the image.
Parameters: image (str) – Generated by savefig
from matplotlib withformat=svg
.Returns: Return type: None Examples
This shows how to update an SVG widget with Matplotlib.
>>> from io import StringIO >>> import matplotlib >>> matplotlib.use('Agg') >>> import matplotlib.pyplot as plt >>> image = SVG() >>> >>> def callback(x): ... sio = StringIO() ... plt.plot(range(5)) ... plt.savefig(sio, format='svg') ... sio.seek(0) ... s = sio.read() ... idx = s.find('<svg') ... s = s[idx:] ... image.do_image(s)
-
Progress¶
Progress component.
Not for direct use by user.
-
class
bowtie._progress.
Progress
[source]¶ Circle progress indicator.
Create a progress indicator.
This component is used by all visual components. It is not meant to be used alone.
By default, it is not visible. It is an opt-in feature and you can happily use Bowtie without using the progress indicators at all.
It is useful for indicating progress to the user for long-running processes. It can be accessed through the
.progress
accessor.Examples
Using the progress widget to provide feedback to the user.
>>> from bowtie.visual import Plotly >>> plotly = Plotly() >>> def callback(x): ... plotly.progress.do_visible(True) ... plotly.progress.do_percent(0) ... compute1() ... plotly.progress.do_inc(50) ... compute2() ... plotly.progress.do_visible(False)
References
https://ant.design/components/progress/
-
do_inc
(inc)[source]¶ Increment the progress indicator.
Parameters: inc (number) – Value to increment the progress. Returns: Return type: None
-
Controllers¶
Button¶
Dropdown¶
-
class
bowtie.control.
Dropdown
(labels: Optional[Sequence[str]] = None, values: Optional[Sequence[Union[str, int]]] = None, multi: bool = False, default: Union[str, int, None] = None)[source]¶ Dropdown based on react-select.
Create a drop down.
Parameters: - labels (array-like, optional) – List of strings which will be visible to the user.
- values (array-like, optional) – List of values associated with the labels that are hidden from the user.
- multi (bool, optional) – If multiple selections are allowed.
- default (str or int, optional) – The default selected value.
-
do_choose
(values)[source]¶ Replace the drop down fields.
Parameters: values (list or str or int) – Value(s) of drop down item(s) to be selected. Returns: Return type: None
-
do_options
(labels, values)[source]¶ Replace the drop down fields.
Parameters: - labels (array-like) – List of strings which will be visible to the user.
- values (array-like) – List of values associated with the labels that are hidden from the user.
Returns: Return type:
-
get
(timeout=10)¶ Return selected value(s).
-
on_change
¶ Emit an event when the selection changes.
Payload:dict
with keys “value” and “label”.
Switch¶
DatePicker¶
MonthPicker¶
RangePicker¶
Textbox¶
-
class
bowtie.control.
Textbox
(placeholder: str = 'Enter text', size: str = 'default', area: bool = False, autosize: bool = False, disabled: bool = False)[source]¶ A single line text box.
Create a text box.
Parameters: - placeholder (str, optional) – Initial text that appears.
- size ('default', 'large', 'small', optional) – Size of the text box.
- area (bool, optional) – Create a text area if True else create a single line input.
- autosize (bool, optional) – Automatically size the widget based on the content.
- disabled (bool, optional) – Disable input to the widget.
References
https://ant.design/components/input/
-
do_text
(text)[source]¶ Set the text of the text box.
Parameters: text (str) – String of the text box.
Number¶
-
class
bowtie.control.
Number
(start: int = 0, minimum: float = -1e+100, maximum: float = 1e+100, step: int = 1, size: str = 'default')[source]¶ A number input widget with increment and decrement buttons.
Create a number input.
Parameters: - start (number, optional) – Starting number
- minimum (number, optional) – Lower bound
- maximum (number, optional) – Upper bound
- size ('default', 'large', 'small', optional) – Size of the text box.
References
https://ant.design/components/input/
-
get
(timeout=10)¶ Get the current number.
Returns: Return type: number
Slider¶
-
class
bowtie.control.
Slider
(start: Union[float, Sequence[float], None] = None, ranged: bool = False, minimum: float = 0, maximum: float = 100, step: float = 1, vertical: bool = False)[source]¶ Ant Design slider.
Create a slider.
Parameters: - start (number or list with two values, optional) – Determines the starting value. If a list of two values are given it will be a range slider.
- ranged (bool, optional) – If this is a range slider.
- minimum (number, optional) – Minimum value of the slider.
- maximum (number, optional) – Maximum value of the slider.
- step (number, optional) – Step size.
- vertical (bool, optional) – If True, the slider will be vertical
References
https://ant.design/components/slider/
-
do_inc
(value=1)[source]¶ Increment value of slider by given amount.
Parameters: value (int) – Number to change value of slider by.
-
do_max
(value)[source]¶ Replace the max value of the slider.
Parameters: value (int) – Maximum value of the slider.
-
do_min
(value)[source]¶ Replace the min value of the slider.
Parameters: value (int) – Minimum value of the slider.
-
do_min_max_value
(minimum, maximum, value)[source]¶ Set the minimum, maximum, and value of slider simultaneously.
Parameters:
-
get
(timeout=10)¶ Get the currently selected value(s).
Returns: List if it’s a range slider and gives two values. Return type: list or number
NoUISlider¶
-
class
bowtie.control.
Nouislider
(start: Union[int, Sequence[int]] = 0, minimum: int = 0, maximum: int = 100, tooltips: bool = True)[source]¶ A lightweight JavaScript range slider library.
Create a slider.
Parameters: - start (number or list with two values, optional) – Determines the starting value. If a list of two values are given it will be a range slider.
- minimum (number, optional) – Minimum value of the slider.
- maximum (number, optional) – Maximum value of the slider.
- tooltips (bool, optional) – Show a popup text box.
References
https://refreshless.com/nouislider/events-callbacks/
-
get
(timeout=10)¶ Get the currently selected value(s).
Returns: List if it’s a range slider and gives two values. Return type: list or number
-
on_change
¶ Emit an event when the slider is moved.
https://refreshless.com/nouislider/events-callbacks/
Payload:list
of values.Returns: Name of event. Return type: str
-
on_end
¶ Emit an event when the slider is moved.
https://refreshless.com/nouislider/events-callbacks/
Payload:list
of values.Returns: Name of event. Return type: str
-
on_set
¶ Emit an event when the slider is moved.
https://refreshless.com/nouislider/events-callbacks/
Payload:list
of values.Returns: Name of event. Return type: str
-
on_slide
¶ Emit an event when the slider is moved.
https://refreshless.com/nouislider/events-callbacks/
Payload:list
of values.Returns: Name of event. Return type: str
-
on_start
¶ Emit an event when the slider is moved.
https://refreshless.com/nouislider/events-callbacks/
Payload:list
of values.Returns: Name of event. Return type: str
-
on_update
¶ Emit an event when the slider is moved.
https://refreshless.com/nouislider/events-callbacks/
Payload:list
of values.Returns: Name of event. Return type: str
Upload¶
-
class
bowtie.control.
Upload
(multiple=True)[source]¶ Draggable file upload widget.
Create the widget.
Note: the handler parameter may be changed in the future.
Parameters: multiple (bool, optional) – If true, you can upload multiple files at once. Even with this set to true, the handler will get called once per file uploaded. -
on_upload
¶ Emit an event when the selection changes.
There is no getter associated with this event.
Payload:tuple
with a str (name) and BytesIO (stream).The user is responsible for storing the object in this function if they want it for later use. To indicate an error, return True, otherwise a return value of None or False indicate success.
-
Checkbox¶
-
class
bowtie.control.
Checkbox
(labels: Optional[Sequence[str]] = None, values: Optional[Sequence[Union[str, int]]] = None, defaults: Optional[Sequence[Union[str, int]]] = None)[source]¶ Ant Design checkboxes.
Create checkboxes.
Parameters: - labels (optional, sequence of stings) –
- values (optional, sequence of strings or ints) –
- defaults (optional, sequence of strings or ints) –
References
https://ant.design/components/checkbox/
-
do_check
(values: Sequence[Union[str, int]])[source]¶ Set the value of the slider.
Parameters: values (Sequence of int, str) – The radio value to select.
-
do_options
(labels: Sequence[str], values: Sequence[Union[str, int]]) → Sequence[Dict][source]¶ Replace the checkbox options.
Parameters: - labels (array-like) – List of strings which will be visible to the user.
- values (array-like) – List of values associated with the labels that are hidden from the user.
Returns: Return type:
Radio¶
-
class
bowtie.control.
Radio
(labels: Optional[Sequence[str]] = None, values: Optional[Sequence[Union[str, int]]] = None, default: Union[str, int, None] = None)[source]¶ Ant Design Radio Group.
Create radio boxes.
Parameters: - labels (optional, sequence of stings) –
- values (optional, sequence of strings or ints) –
- default (optional, string or int) –
References
https://ant.design/components/radio/
-
do_options
(labels, values)[source]¶ Replace the radio button options.
Parameters: - labels (Sequence) – List of strings which will be visible to the user.
- values (Sequence) – List of values associated with the labels that are hidden from the user.
Returns: Return type:
-
do_select
(value: Union[str, int])[source]¶ Set the value of the slider.
Parameters: value (int, str) – The radio value to select.
HTML¶
Markdown¶
-
class
bowtie.html.
Markdown
(initial: str = '')[source]¶ Display Markdown.
Create a Markdown widget.
Parameters: initial (str, optional) – Default markdown for the widget. -
do_text
(text)[source]¶ Replace widget with this text.
Parameters: test (str) – Markdown text as a string. Returns: Return type: None
-
get
(timeout=10)¶ Get the current text.
Returns: Return type: String of html.
-
Div¶
-
class
bowtie.html.
Div
(text: str = '')[source]¶ Div tag.
Create header text with a size.
Parameters: text (str) – Text of the header tag. -
do_text
(text)[source]¶ Replace widget with this text.
Parameters: test (str) – Markdown text as a string. Returns: Return type: None
-
get
(timeout=10)¶ Get the current text.
Returns: Return type: String of html.
-
Header¶
-
class
bowtie.html.
Header
(text: str = '', size: int = 1)[source]¶ Header tag.
Create header text with a size.
Parameters: -
do_text
(text)[source]¶ Replace widget with this text.
Parameters: test (str) – Markdown text as a string. Returns: Return type: None
-
get
(timeout=10)¶ Get the current text.
Returns: Return type: String of html.
-
Feedback¶
There will be a few ways to message users.
Message¶
Messages provide a temporary message that will disappear after a few seconds.
Reference¶
https://ant.design/components/message/
-
bowtie.feedback.message.
error
(content)[source]¶ Error message.
Parameters: content (str) – Message to show user.
-
bowtie.feedback.message.
info
(content)[source]¶ Info message.
Parameters: content (str) – Message to show user.
-
bowtie.feedback.message.
loading
(content)[source]¶ Load message.
Parameters: content (str) – Message to show user.
Cache¶
Bowtie provides a simple key value store where you can store data with the client. Keep in mind that if you store a large amount of data it will get transferred to and from the client which could result in a poor user experience. That being said, it can be very useful to store results from expensive computations.
-
class
bowtie._cache.
_Cache
[source]¶ Store data in the browser.
This cache uses session storage so data will stay in the browser until the tab is closed. All data must be serializable, which means if the serialization transforms the data it won’t be the same when it is fetched.
Examples
>>> from bowtie import cache >>> cache['a'] = True # doctest: +SKIP >>> cache['a'] # doctest: +SKIP True >>> cache['b'] = np.arange(5) # doctest: +SKIP >>> cache['b'] # doctest: +SKIP [1, 2, 3, 4, 5]
Authentication¶
Bowtie provides simple basic authentication out of the box.
Better support for other authentication methods is planned for future releases.
Adding basic authentication to your app is designed to be easy and simple.
Simply create a BasicAuth
instance and pass it the Bowtie app and a
dictionary with usernames as keys and passwords as values:
from bowtie import App
from bowtie.auth import BasicAuth
app = App(__name__)
basic_auth = BasicAuth(app, {
'alice': 'secret1',
'bob': 'secret2',
})
To provide your own authentication you can implement the interface required by the
abstract Auth
class.
This is not well supported or well documented at this time unfortunately.
-
class
bowtie.auth.
Auth
(app: bowtie._app.App)[source]¶ Abstract Authentication class.
Create Auth class to protect flask routes and socketio connect.
-
class
bowtie.auth.
BasicAuth
(app: bowtie._app.App, credentials: Dict[str, str])[source]¶ Basic Authentication.
Create basic auth with credentials.
Parameters: credentials (dict) – Usernames and passwords should be passed in as a dictionary. Examples
>>> from bowtie import App >>> from bowtie.auth import BasicAuth >>> app = App(__name__) >>> auth = BasicAuth(app, {'alice': 'secret1', 'bob': 'secret2'})
Deploy¶
This section is under development. It will discuss options for deploying Bowtie apps and scaling them.
Todo
gunicorn example
Todo
NGINX example
Jupyter Integration¶
Bowtie can run a dashboard defined in a Jupyter notebook. First load the extension:
%load_ext bowtie
Create your app in the same fashion you would in a script.
Instead of using a main
function decorated with @command
,
we use an IPython magic:
app = App()
%bowtie app
This will run the Bowtie app and create an iframe to view the dashboard. When you want to stop the Bowtie app use the following magic:
%bowtie_stop
Using Docker¶
Bowtie depends on yarn to manage Node packages. If you would prefer to not install this on your system you can use the provided Dockerfile to build your Bowtie app. The file provides a conda environment with python 3.6.
Docker Hub¶
The Docker image is hosted on Docker Hub. To pull the bleeding edge release:
docker pull jwkvam/bowtie
To pull a specific version:
docker pull jwkvam/bowtie:0.6.0
Usage¶
I recommend running the Docker interactively:
docker run -ti -p 9991:9991 -v (pwd):/work -rm bowtie bash
This runs Docker in your current working directory. Run this command in the same directory as your bowtie project. This forwards the Docker port 9991 to the host, so you can access the dashboard from the host machine.
You may find it convenient to make this command an alias:
alias bowtie='docker run -ti -p 9991:9991 -v (pwd):/work -rm bowtie bash'
Let’s say your dashboard is in app.py
and you have a requirements.txt
file:
$ bowtie
# now inside the docker
bowtie $ pip install -r requirements.txt
bowtie $ python app.py run
After a few moments you should be able to access the website from your machine.
Exceptions¶
Here is a list of all exceptions defined by Bowtie. Generally there shouldn’t be a need to catch these but they are provided here for completeness.
Bowtie exceptions.
-
exception
bowtie.exceptions.
NoSidebarError
[source]¶ Cannot add to the sidebar when it doesn’t exist.
Architecture¶
Read this section if you are interested in hacking on Bowtie or understanding how it works. Essentially, Bowtie works by using SocketIO to communicate between React and Python.
React¶
All the available components are associated with a React class.
At a minimum each class must have a uuid
and socket
prop.
The uuid
prop is a unique identifier which is used to name the message being sent.
The socket
prop is a socketio connection.
Flask¶
Bowtie attempts to abstract away the Flask interface. Admittedly Flask is not difficult to learn but ultimately I wanted a library which required little boilerplate.
If you want to tinker with the Flask app, you can edit the server.py
file that
gets generated during the build phase.
SocketIO¶
SocketIO binds the Python backend code to the React frontend. Python uses the Flask-SocketIO extension and the frontend uses socket.io-client.
Events¶
Almost every component has events associated with it.
For example, a slider generates events when the user moves the slider.
In Bowtie, these events are class attributes with the prefix on_
.
With the App class you can subscribe callbacks to events, so when an
event happens the callback is called with an argument that is related to the event.
Commands¶
Many components have commands to update their state.
For example you can update the drop down list or update a plot.
The commands are instance functions that are prefixed do_
.
For example, to update a Plotly chart in Bowtie you can call do_all(dict)
,
and Plotly will update it’s chart with the data and layout options defined in the dictionary.
Bowtie Application¶
There are a few key parts to any Bowtie application.
Define the app, components, and global state:
app = App(rows=1, columns=1, sidebar=True) plotly = Plotly() dropdown = Dropdown(*config) global_dataset = SQLQuery(cool_data)
Layout the app, you can also do this globally, but this can keep the code better organized:
def layout(): app.add_sidebar(dropdown) app.add(plotly) app.layout = layout
Create what should happen in response to events:
@app.subscribe(dropdown.on_change) def callback(dropdown_item): # compute something cool and plot it data = cool_stuff(global_dataset, dropdown_item) plotly.do_all(data)
Create a main function that simply returns the app. Then Bowtie knows how to build and serve the app:
from bowtie import command @command def main(): return app
Create New Components¶
Bowtie is designed to not make it terribly onerous to make new components. We need to write two new classes.
- Create new React class
- Create visual or control class in Python
To walk through this process I’ll use the dropdown component, since that touches on many interesting pieces.
React Class¶
The dropdown component leverages a popular react dropdown component to do the heavy lifting. First we start by importing react, msgpack and the component:
import React from 'react';
import Select from 'react-select';
import 'react-select/dist/react-select.css';
var msgpack = require('msgpack-lite');
We also imported the default styling so it looks reasonable. We can do this because we’re using webpack to compile the application.
Next we will define the properties that the React class will hold.
This defines how the Python code can initialize the component.
We always need uuid
and socket
properties since they make
it possible for the Python backend to communicate with the React object.
This component supports multiple selection and that will be a bool
property.
We’ll also make an initOptions
property which will let us set an
initial list of options to populate the dropdown.
Now that we have that defined let’s write it in code:
Dropdown.propTypes = {
uuid: React.PropTypes.string.isRequired,
socket: React.PropTypes.object.isRequired,
multi: React.PropTypes.bool.isRequired,
initOptions: React.PropTypes.array
};
Now we will create the class:
export default class Dropdown extends React.Component {
...
}
Everything from now we’ll write as functions in the class body. First we’ll look at the render function:
render () {
return (
<Select
multi={this.props.multi}
value={this.state.value}
options={this.state.options}
onChange={this.handleChange}
/>
);
}
This instantiates the component and allows us to set configuration options for the underlying component.
Note that this.state
is mutable and this.prop
is fixed.
For example, multiple selection cannot be changed but the drop down options can be changed.
Now we’ll tell it how to communicate.
We do this after the component is created in the componentDidMount
function:
componentDidMount() {
var socket = this.props.socket;
var uuid = this.props.uuid;
socket.on(uuid + '#get', this.getValue);
socket.on(uuid + '#options', (data) => {
var arr = new Uint8Array(data['data']);
this.setState({value: null, options: msgpack.decode(arr)});
});
}
Note that we have defined a command to be used through Python with do_options
and
a get
function so Python can get it’s current state.
Next we define the constructor which initializes the state and binds this
to those handlers:
constructor(props) {
super(props);
this.state = {value: null};
this.state.options = this.props.initOptions;
this.handleChange = this.handleChange.bind(this);
this.getValue = this.getValue.bind(this);
}
Lastly we define the handlers referenced above:
handleChange(value) {
this.setState({value});
this.props.socket.emit(this.props.uuid + '#change', value);
}
getValue(data, fn) {
fn(this.state.value);
}
Python Class¶
Now that we have the React component defined, let’s write the Python half. We don’t need to write much here, it’s a little glue code.
First we’ll define the class:
class Dropdown(_Controller):
_TEMPLATE = 'dropdown.jsx'
_COMPONENT = 'Dropdown'
_PACKAGE = 'react-select'
_TAG = ('<Dropdown initOptions={{{options}}} '
'multi={{{multi}}}'
'socket={{socket}} '
'uuid={{{uuid}}} '
'/>')
We have defined a few component specific constants:
_TEMPLATE
: Name of the file where the React class is defined._COMPONENT
: Name of the React class (used to import the class)._PACKAGE
: Name of the NPM package used by the component._TAG
: String used to instantiate the component.
We write the constructor who’s main responsibility is creating the string to instantiate
the component in Javascript.
In Bowtie, this gets assigned to the _instantiate
field:
def __init__(self, options, multi=False):
super(Dropdown, self).__init__()
self._instantiate = self._TAG.format(
options=json.dumps(options),
multi='true' if multi else 'false',
uuid="'{}'".format(self._uuid)
)
Lastly we have one event (named “change”), one command (named “options”), and one getter (named “get”). We can create those by defining functions with the appropriate name and arguments, metaclasses handle the rest:
def on_change(self):
pass
For the commands, we have the ability to just pass the data through to the React component:
def do_options(self, data):
return data
We can also preprocess the data to present an easier interface for the programmer:
def do_options(self, labels, values):
return [dict(label=l, value=v) for l, v in zip(labels, values)]
The main caveat here is we must ensure the data is serializable by msgpack.
For the getter we can write:
def get(self, data):
return data
We can use this getter to do post processing, but here I just return the data as given to me from the React component.
Metaclass Parsing¶
A note about how commands, events, and getters are transformed into messages.
Events¶
Anything function that begins with on_
is an event.
The message that ends up getting sent is on_{name}
is {uuid}#name
.
Commands¶
Anything function that begins with do_
is a command.
The message that ends up getting sent is do_{name}
is {uuid}#name
.
Getters¶
Anything function that begins with get_
or is get
is a getter.
The message that ends up getting sent is get_{name}
is {uuid}#get
or {uuid}#get_{name}
.
Comparison with Other Tools¶
Bowtie is designed to have a simple API to create dashboard applications quickly. That being said let’s compare this against similar Python libraries. This section could use some help especially if you are familiar with one of the libraries listed.
Dash¶
From Plotly is Dash Even though both Bowtie and Dash allow you to develop a dashboard we have very different designs.
Layout API¶
Dash uses HTML. In order to layout your dash app you need to know a little HTML. This can be a pro or con depending on your comfort with HTML.
Bowtie uses its own Pythonic API. You don’t need to know any HTML. On the other hand you need to read the API to understand how to use it.
Events¶
In my opinion, the callback and event system is much easier to use and more powerful in Bowtie. The API is Pythonic so you don’t have to memorize special strings. You can distinguish between events and state. You can update multiple components in a single callback. Bowtie tries very hard to be simple to use and powerful.
Style¶
One area that Bowtie lacks in is styling. Dash has powerful styling techniques. If you need custom styling in Bowtie, you’ll need to edit the generated HTML by hand.
Building and Running¶
Running a Bowtie app consists of two steps: first “building” and then “running” the app.
The build process figures out what Javascript libraries are needed, creates HTML and Javascript files,
and finally uses Webpack to assemble it all into one Javascript file, bundle.js
.
Once the Javascript bundle is built the Bowtie app can be run.
Dash uses minified Javascript files and it doesn’t pack them. Overall this likely results in a better user experience for Dash users since packing the Javascript doesn’t result in a much smaller file or much better Javascript performance.
Dynamic Layout¶
Bowtie’s layouts are currently defined at build time. Dash can dynamically change the layout. This is a feature that I would like to add into Bowtie.
Source Code¶
Bowtie is a monolithic repo. All the Python, Javascript, and HTML is in a single repo. Dash is modular and has multiple repos for its core functionality, React components, and HTML components.
I believe Bowtie is easier to understand and maintain because everything is self-contained. Instead it’s harder to use custom components with Bowtie since they need to be included in the library itself. This is decoupled in Dash so custom components are easier to develop.
Other¶
This has touched on some of the major differences. There are many more however that I’ll try to address eventually.
Bokeh¶
This is the oldest dashboard tool in Python I’m aware of. I think it hasn’t been adopted much because of poor visibility and documentation. To be fair I haven’t used it a lot and only discovered it after I created Bowtie.
Shiny¶
Not a Python library but is the gold standard.
Todo
should try using shiny so I know what it’s like