Pyxley!¶
The Pyxley python package makes it easier to deploy Flask-powered dashboards using a collection of common JavaScript charting libraries. UI components are powered by PyxleyJS.
Why Pyxley?¶
Pyxley was born out of the desire to combine the ease of data manipulation in pandas with the beautiful visualizations available in JavaScript. It was largely inspired by Real Python. The goal was to create a library of reusable visualization components that allow the quick development of web-based data products. For data scientists with limited JavaScript exposure, this package is meant to provide generic visualizations along with the ability to customize as needed.
React¶
Pyxley utilizes Facebook’s React. React is only concerned with the UI, so we can use it for only the front-end portion of our web-applications.
Flask¶
Flask is a great micro web-framework. It allows us to very easily stand up web applications and services.
PyxleyJS¶
Pyxley relies on a JavaScript library that heavily leverages React and existing visualization libraries. Wrappers for common libraries have been created so that the user only needs to specify the type of chart they want, the filters they wish to include, and the parameters specific to that visualization.
To download or contribute, visit PyxleyJS.
Insane Templates¶
A lot of other projects rely on a set of complicated templates that attempt to cover as many use cases as possible. The wonderful thing about React is the ability to create compositions of components. This means that we only need a single template: a parent component that manages all of the child components. With this layout, we can use factories within JavaScript to create the components using the supplied parameters. Organizing the code in this way allows us to create a common framework through which we can create a variety of different components.
For example, the underlying interface for a Dropdown Button and a Line Chart are the same. The only difference is the options supplied by the user. This provides a really easy way to integrate with Python. We can use Python for organizing the types and options. PyReact can then be used to transform to JavaScript.
Core Components¶
In Pyxley, the core component is the UILayout
. This component is
composed of a list of charts
and filters
, a single React
component from a JavaScript file, and the Flask app.
# Make a UI
from pyxley import UILayout
ui = UILayout(
"FilterChart",
"./static/bower_components/pyxley/build/pyxley.js",
"component_id")
This will create a UI object that’s based on the FilterChart
React
component in pyxley.js
. It will be bound to an html div
element
called component_id
.
If we wanted to add a filter and a chart we could do so with the following
# Make a Button
cols = [c for c in df.columns if c != "Date"]
btn = SelectButton("Data", cols, "Data", "Steps")
# Make a FilterFrame and add the button to the UI
ui.add_filter(btn)
# Make a Figure, add some settings, make a line plot
fig = Figure("/mgchart/", "mychart")
fig.graphics.transition_on_update(True)
fig.graphics.animate_on_load()
fig.layout.set_size(width=450, height=200)
fig.layout.set_margin(left=40, right=40)
lc = LineChart(sf, fig, "Date", ["value"], init_params={"Data": "Steps"}, timeseries=True)
ui.add_chart(lc)
Calling the ui.add_chart
and ui.add_filter
methods simply adds
the components we’ve created to the layout.
app = Flask(__name__)
sb = ui.render_layout(app, "./static/layout.js")
Calling ui.render_layout
builds the JavaScript file containing everything we’ve created.
Charts¶
Charts are meant to span any visualization of data we wish to construct. This includes line plots, histograms, tables, etc. Several wrappers have been introduced and more will be added over time.
Implementation¶
All charts
are UIComponents
that have the following attributes and methods
- An endpoint route method. The user may specify one to override the default.
- A
url
attribute that the route function is assigned to by the flask app. - A
chart_id
attribute that specifies the element id. - A
to_json
method that formats the json response.
Filters¶
Filters are implemented in nearly the same way that charts
are implemented. The only
difference is the lack of the to_json
method.
A Basic Pyxley App¶
Here we will go through building a basic web-application using Pyxley and Flask.
I recommend visiting the Real Python blog for a great intro to a basic app.
App
| package.json
| .bowerrc
| bower.json
|
└---project
| app.py
| templates
|
└---static
| css
| js
Some notes about the above structure
- This assumes that you are running the app from the
project
folder. - Any JavaScript created by the app should go in the
js
folder.
JavaScript!¶
Node & NPM¶
At the highest level, Node is our biggest JavaScript dependency. All of the JavaScript dependencies are managed via NPM. This document won’t show you how to get Node or NPM, but for Mac OS X users, you can get it through homebrew.
Note: Prior to version 0.0.9, Bower was used to manage dependencies. In an effort to simplify the massive amount of dependencies, NPM will be used as the primary package manager and Bower is completely optional. In addition, PyReact has been deprecated and will no longer be used to transpile the jsx code.
HTML & CSS¶
HTML templates used by flask are stored in the templates
folder. For our purposes,
we only need a really basic template that has a single div
element.
<div id="component_id"></div>
Everything in our app will be tied to this single component.
CSS¶
We store any additional CSS we need in the static\CSS
folder.
Flask¶
Courtesy of the Flask website, “Hello, World!” in Flask looks like the code below.
from flask import Flask
app = Flask(__name__)
@app.route('/')
def hello_world():
return 'Hello World!'
if __name__ == '__main__':
app.run()
We simply need to build upon this.
Adding Some Pyxley¶
Let’s start by importing some simple things and building upon the example above. We
will import some pyxley
components, create a UI, and load a data frame.
# Import flask and pandas
from flask import Flask, render_template
import pandas as pd
# Import pyxley stuff
from pyxley import UILayout
from pyxley.filters import SelectButton
from pyxley.charts.mg import LineChart, Figure
# Read in the data and stack it, so that we can filter on columns
df = pd.read_csv("fitbit_data.csv")
sf = df.set_index("Date").stack().reset_index()
sf = sf.rename(columns={"level_1": "Data", 0: "value"})
# Make a UI
ui = UILayout(
"FilterChart",
"./static/bower_components/pyxley/build/pyxley.js",
"component_id")
# Create the flask app
app = Flask(__name__)
At this point we now have some data and a layout to build upon. Adding the code below will add a dropdown select button and a line plot.
# Make a Button
cols = [c for c in df.columns if c != "Date"]
btn = SelectButton("Data", cols, "Data", "Steps")
# Make a FilterFrame and add the button to the UI
ui.add_filter(btn)
# Make a Figure, add some settings, make a line plot
fig = Figure("/mgchart/", "mychart")
fig.graphics.transition_on_update(True)
fig.graphics.animate_on_load()
fig.layout.set_size(width=450, height=200)
fig.layout.set_margin(left=40, right=40)
lc = LineChart(sf, fig, "Date", ["value"], init_params={"Data": "Steps"}, timeseries=True)
ui.add_chart(lc)
Now that our ui
object is full of filters and charts, we need
to write out the JavaScript and transpile the jsx code. Previously,
we used PyReact, but unfortunately that has been deprecated. Instead,
we rely on webpack
. We have written a wrapper for webpack
that does
the bundling for us.
- ::
sb = ui.render_layout(app, ”./project/static/layout.js”)
# Create a webpack file and bundle our javascript from pyxley.utils import Webpack wp = Webpack(”.”) wp.create_webpack_config(
“layout.js”, ”./project/static/”, “bundle”, ”./project/static/”) wp.run()
@app.route(‘/’, methods=[“GET”]) @app.route(‘/index’, methods=[“GET”]) def index():
_scripts = [”./bundle.js”] css = [”./css/main.css”] return render_template(‘index.html’,
title=TITLE, base_scripts=[], page_scripts=_scripts, css=css)- if __name__ == ‘__main__’:
- app.run()
wp.run()
will transpile ”./project/static/layout.js” with the
necessary dependencies and produce “bundle.js”. If you had further
dependencies not managed by NPM, you could include them in the
base_scripts
keyword argument.
Now when you run app.py
from the project
folder, accessing your localhost on port 5000 will lead to a simple plot. This example was adapted from the metricsgraphics example in the Pyxley repository.
Pyxley In Production¶
Now that we’ve built an app, how do we deploy it? Because Pyxley is built on top of Flask, the process for deploying our app is no different than deploying any other Flask app. Rather than cover the basics of deploying web apps, this guide will cover some patterns and address some of Pyxley’s capabilities.
Data and DataFrames¶
Pyxley was developed specifically with Pandas in mind. Each widget has methods for transforming dataframes into JSON objects that the different JavaScript libraries can interpret. It’s not entirely obvious how to deal with issues such as reloading data.
Updating Data¶
Let’s revisit our MetricsGraphics example, specifically the
LineChart
object. Recall when we created the object,
we passed it a dataframe and it looked something like
the snippet below.
- ::
- # sf is our dataframe # fig is our figure object lc = LineChart(sf, fig, “Date”, [“value”], **kwargs)
When the dataframe, sf
, is passed into the constructor
it is passed into a function that Flask will use any time
it needs the chart data.
In the __init__
method you will find the following
snippet.
- ::
- if not route_func:
- def get_data():
args = {} for c in init_params:
- if request.args.get(c):
- args[c] = request.args[c]
- else:
- args[c] = init_params[c]
- return jsonify(LineChart.to_json(
- self.apply_filters(df, args), x, y, timeseries=timeseries
))
route_func = get_data
The default behavior for LineChart
is to use this
basic route function. Let’s focus on the data that
gets returned for a moment. Notice we call jsonify
which is a Flask function that turns a python dict
into a JSON object. The input dict
is returned
by the LineChart.to_json
method. to_json
takes
a dataframe as the input as well as a few other variables.
self.apply_filters
is a method belonging to the Chart
base class that takes a dataframe and a dict
containing
our filtering conditions.
As far as default behavior, this is perfectly reasonable, but
it makes a pretty rigid assumption that the underlying data
will not change. This default route function is created when
the LineChart
object is initialized and changing the data
would require us to recreate the object. In practice, it’s not
a good idea to have a dependency that requires an app to be
restarted.
Now notice the first line of our snippet: if not route_func
.
route_func
is a keyword argument in the __init__
method.
If a developer provides their own route function, they are no
longer restricted by the default behavior.
Reloading Dataframes¶
Let’s assume that we are perfectly comfortable with using Pandas as our data source, but we would like to be able to refresh the data on some cadence.
Returning to our LineChart
example, what if created
it in the following way:
- ::
# sf is our dataframe # fig is our figure object lc = LineChart(sf, fig, “Date”, [“value”],
route_func=some_other_route, **kwargs)
Now that we have passed in some_other_route
it will
be used instead and sf
will be ignored. So let’s
start building what the function looks like.
- ::
- class MyDataWrapper(object):
“”” We are building a simple wrapper for our data “”” def __init__(df, x, y, timeseries=True):
self.df = df self.x = x self.y = y self.timeseries = timeseries- def my_route_function(self):
# put args check here return jsonify(LineChart.to_json(
Chart.apply_filters(self.df, args), self.x, self.y, timeseries=self.timeseries))
So the first thing you should notice is that this
looks pretty similar to our default get_data
function
from above. The most obvious difference is that now
most of the variables are now member variables of our
MyDataWrapper
class. The main benefit from this is
that now we are calling a method in our MyDataWrapper
object that also manages the state of our dataframe
self.df
. Now that LineChart
no longer has anything
to do with our dataframe and how to parse it, we are free to
reload our data. You could imagine adding a set_data
method to our class that has logic about how and when to
reload data. The snippet below shows how to modify our example.
We will still pass in all of the necessary chart options,
but the important logic is now handled by our new object.
- ::
# sf is our dataframe myData = MyDataWrapper(sf, “Date”, [“value”]) # fig is our figure object lc = LineChart(sf, fig, “Date”, [“value”],
route_func=myData.my_route_function, **kwargs)
Databases¶
What if your data is too big. Say, for example, you would
like to use guincorn
and the idea of replicating your
data across multiple processes just isn’t feasible. If you
are using a relational database that works with SQLAlchemy
there’s not much to change. The snippet below shows one
possible change to our data wrapper from above. It’s also
important to remember that we just need to format our data
for the JavaScript libraries. We can freely customize the
methods to accommodate any data source.
- ::
- class MyDataWrapper(object):
“”” We are building a simple wrapper for our data “”” def __init__(sql_engine, x, y, timeseries=True):
self.engine = sql_engine self.x = x self.y = y self.timeseries = timeseries- def get_data(self, args):
- # make some sql query or use SQLAlchemy functions return pd.read_sql(‘select * from table’, self.engine)
- def my_route_function(self):
# put args check here df = get_data(args) return jsonify(LineChart.to_json(
Chart.apply_filters(df, args), self.x, self.y, timeseries=self.timeseries))
REST APIs¶
What if you want to hit some other API? Use requests
! Then
it’s the same game of just leveraging the other functions to
get the data in the format the chart needs.
About¶
Read more about Pyxley at the Multithreaded blog.
Features¶
- Simple web-applications using Flask, PyReact, and Pandas
- Integration with d3.js based libraries such as MetricsGraphics
- Ability to integrate custom React UIs