Source code for PFN Dash App Demo#
dash_pivottable_enrollment.py#
# This code shows how to use the dash-pivottable library to easily
# create an interactive enrollment dashboard.
# Much of this code derives from
# https://github.com/plotly/dash-pivottable/blob/master/usage.py
# and https://dash.plotly.com/urls (which provides useful information
# on the Dash Pages feature that this app uses extensively).
import dash
from dash import html, callback, Input, Output
import dash_pivottable
import pandas as pd
# Importing current enrollment data:
from data_import import df_curr_enrollment
# Note that the 'data' entry below should take the form of a list of lists
# or dicts rather than a DataFrame. (For reference, see
# https://github.com/plotly/react-pivottable/#accepted-formats-for-data)
# Therefore, we'll need to convert our DataFrame into this format
# before we can run the code.
# It's possible to convert a DataFrame into a list of lists,
# but I believe the easiest solution is to use to_dict(orient='records')
# to convert the DataFrame into a list of dictionaries.
lod_curr_enrollment = df_curr_enrollment.to_dict(
orient='records') # lod = 'list of dicts'
dash.register_page(__name__, path='/dash_pivottable_enrollment') #
layout = html.Div([
dash_pivottable.PivotTable(
id='enrollment_table',
data=lod_curr_enrollment,
cols=['Level For Sorting', 'Level'],
colOrder="key_a_to_z",
rows=['College'],
rowOrder="key_a_to_z",
rendererName="Grouped Column Chart",
aggregatorName="Sum",
vals=["Enrollment"],
valueFilter={}
),
html.Div(
id='output'
)
])
@callback(Output('enrollment_output', 'enrollment_children'),
[Input('enrollment_table', 'cols'),
Input('enrollment_table', 'rows'),
Input('enrollment_table', 'rowOrder'),
Input('enrollment_table', 'colOrder'),
Input('enrollment_table', 'aggregatorName'),
Input('enrollment_table', 'rendererName')])
def display_props(cols, rows, row_order, col_order, aggregator, renderer):
return [
html.P(str(cols), id='columns'),
html.P(str(rows), id='rows'),
html.P(str(row_order), id='row_order'),
html.P(str(col_order), id='col_order'),
html.P(str(aggregator), id='aggregator'),
html.P(str(renderer), id='renderer'),
]
dash_pivottable_survey_results.py#
# This code shows how to use the dash-pivottable library to easily create
# an interactive survey results dashboard.
# For a bit more documentation on this code, see
# dash_pivottable_enrollment.py .
import dash
from dash import html, callback, Input, Output
import dash_pivottable
import pandas as pd
# Reading in survey data, then merging it with
# enrollment data:
# (The benefit of using original student-level data is that
# our averages will automatically be weighted by student counts, thus
# producing more accurate averages.)
from data_import import df_survey_results_extra_data
lod_survey_results_extra_data = df_survey_results_extra_data.to_dict(
orient='records')
dash.register_page(__name__, path='/dash_pivottable_survey_results')
layout = html.Div([
dash_pivottable.PivotTable(
id='table',
data=lod_survey_results_extra_data,
cols=['Season'],
colOrder="key_a_to_z",
rows=['College'],
rowOrder="key_a_to_z",
rendererName="Grouped Column Chart",
aggregatorName="Average",
vals=["Score"],
valueFilter={}
),
html.Div(
id='output'
)
])
@callback(Output('output', 'children'),
[Input('table', 'cols'),
Input('table', 'rows'),
Input('table', 'rowOrder'),
Input('table', 'colOrder'),
Input('table', 'aggregatorName'),
Input('table', 'rendererName')])
def display_props(cols, rows, row_order, col_order, aggregator, renderer):
return [
html.P(str(cols), id='columns'),
html.P(str(rows), id='rows'),
html.P(str(row_order), id='row_order'),
html.P(str(col_order), id='col_order'),
html.P(str(aggregator), id='aggregator'),
html.P(str(renderer), id='renderer'),
]
fixed_dashboard.py#
# Sample Fixed Dashboard
# By Kenneth Burchfiel
# Released under the MIT License
import dash
from dash import html, dcc
import dash_bootstrap_components as dbc
from data_import import df_curr_enrollment
import plotly.express as px
# Creating a pivot table that can serve as the basis of our enrollment
# by college and level graph:
df_enrollment_by_college_and_level = df_curr_enrollment.pivot_table(
index=['College', 'Level For Sorting', 'Level'],
values='Enrollment', aggfunc='sum').reset_index()
# Creating a graph of this pivot table:
fig_enrollment_by_college_and_level = px.bar(
df_enrollment_by_college_and_level,
x='College', y='Enrollment', color='Level',
barmode='group',
text_auto='.0f',
title='NVCU Enrollment by College and Level')
# Performing the same steps for simpler charts that show enrollment
# by college and by level (but not both):
df_enrollment_by_college = df_curr_enrollment.pivot_table(
index=['College'],
values='Enrollment', aggfunc='sum').reset_index()
fig_enrollment_by_college = px.bar(df_enrollment_by_college,
x='College', y='Enrollment', color='College',
text_auto='.0f',
title='NVCU Enrollment by College')
df_enrollment_by_level = df_curr_enrollment.pivot_table(
index=['Level For Sorting', 'Level'],
values='Enrollment', aggfunc='sum').reset_index()
fig_enrollment_by_level = px.bar(df_enrollment_by_level,
x='Level', y='Enrollment', color='Level',
text_auto='.0f',
title='NVCU Enrollment by Level')
dash.register_page(__name__, path='/fixed_dashboard')
layout = dbc.Container([
dcc.Markdown(''' # Simple Fixed Dashboard
This dashboard contains three bar charts that display enrollment
totals by college and level; college; and level. Plotly's built-in
tools make these charts somewhat interactive; however, no additional
comparison or filter options are provided. The other dashboards
within this app allow for more user interaction.
'''),
dcc.Graph(figure=fig_enrollment_by_college_and_level),
# This method of hosting a fixed graph within a Dash page (without any
# callbacks) came from https://dash.plotly.com/
# tutorial#visualizing-data .
dcc.Graph(figure=fig_enrollment_by_college),
dcc.Graph(figure=fig_enrollment_by_level)
])
flexible_enrollment_dashboard.py#
# Flexible Enrollment Dashboard
# By Kenneth Burchfiel
# Released under the MIT License
# Parts of this code derive from
# and https://dash.plotly.com/urls
# and https://dash.plotly.com/minimal-app .
import dash
from dash import html, dcc, callback, Output, Input, dash_table
import dash_bootstrap_components as dbc
from data_import import df_curr_enrollment
import plotly.express as px
# The following auto_pivot_and_graph code was featured
# within the Pivot and Graph Functions section of PFN.
from auto_pivot_and_graph import autopivot_plus_bar
from import_layout import import_layout
dash.register_page(__name__, path='/flexible_enrollment_dashboard')
# Determining which comparison and color options to pass to
# the import_layout() function that will define part of the
# dashboard's layout:
# These lists of options will get initialized as all columns within
# the DataFrame *except* for those present in cols_to_exclude. (If there
# are many columns within the DataFrame, this approach can require
# less typing than would adding in all columns to be included.)
cols_to_exclude = [
'Date Of Birth', 'First Name', 'Last Name',
'Student ID', 'Matriculation Number', 'Enrollment']
comparison_list = list(
set(df_curr_enrollment.columns) - set(cols_to_exclude))
# Converting a list of items into a set can make it easier to remove
# a group of items from that set. For more on this data type, see
# https://docs.python.org/3/tutorial/datastructures.html#sets .
color_list = comparison_list.copy() # These lists can contain
# the same values.
# Setting default comparison and color values:
comparison_default = ['Level For Sorting', 'Level']
color_default = 'College'
filter_cols = ['College', 'Level', 'Gender']
# Note that each of these values must be added to
# the @callback() component of this page along with
# the display_graph inputs.
# (Add '_filter' after each column name within the Callback section.
# For instance, 'College' will map to 'College_filter.')
# Configuring the page's layout:
# The import_layout() function will make it easier to create the
# central part of the page's layout.
# (Note the use of + to combine different lists of layout components
# together.)
layout = dbc.Container([
dbc.Row(dbc.Col(dcc.Markdown('''
# Flexible Interactive Enrollment Dashboard
This dashboard provides a flexible overview of NVCU enrollment.
It utilizes the autopivot(), autobar(), and autotable() functions
found within auto_pivot_and_graph.py to allow for a wide range
of display options. It also uses the import_layout() function
found in import_layout.py to define a sizeable component
of the page's layout.
'''), lg = 9))] +
import_layout(df=df_curr_enrollment,
comparison_list=comparison_list,
comparison_default=comparison_default,
color_list color_list,
color_default=color_default,
filter_cols=filter_cols) +
# For more information about the multi-dropdown option,
# see https://dash.plotly.com/dash-core-components/dropdown .
[dcc.Graph(id='flexible_enrollment_graph')] +
[dcc.Graph(id='flexible_enrollment_table')]
)
# Configuring a callback that can convert the index and filter options
# specified by the user into a custom chart:
@callback(
Output('flexible_enrollment_graph', 'figure'),
Output('flexible_enrollment_table', 'figure'),
Input('comparison_options', 'value'),
Input('color_option', 'value'),
Input('College_filter', 'value'),
Input('Level_filter', 'value'),
Input('Gender_filter', 'value')
)
# The following function calls autopivot_plus_bar() to convert
# the input values specified above into a bar chart:
def display_graph(x_vars, color, college_filter,
level_filter, gender_filter):
print(college_filter,level_filter, gender_filter)
# Creating a list of tuples that can be used to filter
# the output:
filter_tuple_list = [
('College',
college_filter),
('Level',level_filter),
('Gender',gender_filter)
]
print('x_vars contents and type:',x_vars,type(x_vars))
print('color contents and type:',color,type(color))
# '' is passed to custom_aggfunc_name so that
# the chart title will begin with 'Enrollment'
# rather than 'Total Enrollment.'
bar_graph, table = autopivot_plus_bar(
df=df_curr_enrollment, y='Enrollment',
aggfunc='sum', x_vars=x_vars, color=color,
x_vars_to_exclude=['Level For Sorting'],
overall_data_name='All Data',
weight_col=None, filter_tuple_list=filter_tuple_list,
custom_aggfunc_name='', create_table=True,
text_auto='.0f')
print(table)
return bar_graph, table
flexible_survey_results_dashboard.py#
# Flexible Survey Results Dashboard
# By Kenneth Burchfiel
# Released under the MIT License
# Parts of this code derive from
# and https://dash.plotly.com/urls
# and https://dash.plotly.com/minimal-app .
# For a bit more documentation on this code, reference
# flexible_enrollment_dashboard.py, which uses a very similar setup.
import dash
from dash import html, dcc, callback, Output, Input
from data_import import df_survey_results_extra_data
import plotly.express as px
import dash_bootstrap_components as dbc
from auto_pivot_and_graph import autopivot_plus_bar
dash.register_page(__name__, path='/flexible_survey_results_dashboard')
# Configuring the page's layout:
layout = dbc.Container([
dbc.Row(dbc.Col(dcc.Markdown('''
# Flexible Interactive Survey Results Dashboard
This dashboard provides a flexible overview of NVCU student survey
results. It utilizes the autopivot() and autobar() functions found
within auto_pivot_and_graph.py to allow for a wide range of
display options.
'''), lg=9)),
dbc.Row([
dbc.Col(html.H5("Comparison Options:"), lg=3),
dbc.Col(dcc.Dropdown(
['Starting Year', 'Season', 'Gender', 'Matriculation Year',
'College', 'Class Of', 'Level', 'Level For Sorting'],
['College', 'Season'], multi=True,
id='flexible_survey_results_index'), lg=3),
dbc.Col(html.H5("Color Option:"), lg=2),
dbc.Col(dcc.Dropdown(
['Starting Year', 'Season',
'Score', 'Gender', 'Matriculation Year',
'College', 'Class Of', 'Level', 'Level For Sorting'],
'Season', id='flexible_survey_results_color'), lg=2)
]),
dbc.Row([
dbc.Col(html.H5("College Filter:"), lg=3),
dbc.Col(
dcc.Dropdown(df_survey_results_extra_data['College'].unique(),
df_survey_results_extra_data['College'].unique(),
multi=True,
id='college_filter'), lg=3),
]),
dbc.Row([
dbc.Col(html.H5("Level Filter:"), lg=3),
dbc.Col(dcc.Dropdown(df_survey_results_extra_data['Level'].unique(),
df_survey_results_extra_data['Level'].unique(),
multi=True,
id='level_filter'), lg=3)
]),
dcc.Graph(id='flexible_survey_results_view')])
@callback(
Output('flexible_survey_results_view', 'figure'),
Input('flexible_survey_results_index', 'value'),
Input('flexible_survey_results_color', 'value'),
Input('college_filter', 'value'),
Input('level_filter', 'value')
)
def display_graph(x_vars, color, college_filter, level_filter):
print(college_filter,level_filter)
filter_tuple_list = [
('College',
college_filter),
('Level',level_filter)]
print('x_vars contents and type:',x_vars,type(x_vars))
print('color contents and type:',color,type(color))
return autopivot_plus_bar(
df=df_survey_results_extra_data, y='Score',
aggfunc='mean', x_vars=x_vars, color=color,
x_vars_to_exclude=['Level For Sorting'],
overall_data_name='All Data',
weight_col=None, filter_tuple_list=filter_tuple_list)
home_page.py#
# This code derives from
# and https://dash.plotly.com/urls .
import dash
from dash import html, dcc
import dash_bootstrap_components as dbc
dash.register_page(__name__, path='/')
layout = dbc.Container([
dcc.Markdown('''
## [Python For Nonprofits](https://github.com/kburchfiel/pfn)' \
Main Dash App Demo
This project demonstrates how to use Dash to create
interactive online visualizations. These visualizations range
from simple charts to more complex interactive setups.
The [Fixed Dashboard](/fixed_dashboard) page shows a very simple
dashboard setup that lacks user-defined filter and comparison settings.
The [Simple Interactive Dashboard](/simple_interactive_dashboard) page
displays a relatively straightforward interactive enrollment dashboard.
This dashboard didn't require much code to write, but its functionality
is rather limited.
The [Flexible Survey Results](/flexible_survey_results_dashboard) and
[Flexible Enrollment](/flexible_enrollment_dashboard) dashboard pages
allow for a wide range of comparison and color options. These options are
made possible by the autopivot() and autobar() functions found within
[auto_pivot_and_graph.py](https://github.com/kburchfiel/pfn/blob/main/\
Online_Visualizations/PFN_Dash_App_Demo/auto_pivot_and_graph.py). (You may
also find these functions useful for developing standlone Plotly charts.)
The Flexible Enrollment Dashboard also makes use of an import_layout()
function (stored within [import_layout.py](https://github.com/\
kburchfiel/pfn/blob/main/Online_Visualizations/
PFN_Dash_App_Demo/import_layout.py))
in order to reduce the amount of code needed to define
the page's structure and menu options. In addition, this dashboard
applies the autotable() function in [auto_pivot_and_graph.py](https://\
github.com/kburchfiel/pfn/blob/main/Online_Visualizations/\
PFN_Dash_App_Demo/auto_pivot_and_graph.py)) to display
a tabular view of the data featured in the graph.
The dash-pivottable library makes it very easy to
create interactive dashboards. Examples of this library in use can
be found within the [Dash Pivottable (Enrollment)]\
(/dash_pivottable_enrollment) and [Dash Pivottable (Survey Results)]\
(/dash_pivottable_survey_results) pages.
The source code for these dashboards can be found [at this link](https://\
github.com/kburchfiel/pfn/tree/main/\
Online_Visualizations/PFN_Dash_App_Demo).
'''
)])
simple_interactive_dashboard.py#
# Simple Interactive Dashboard
# By Kenneth Burchfiel
# Released under the MIT License
# Parts of this code derive from
# and https://dash.plotly.com/urls
# and https://dash.plotly.com/minimal-app .
# The Layout page within the Dash Bootstrap Components documentation
# (available at https://dash-bootstrap-components.opensource.faculty.ai/
# docs/components/layout/ )
# provide very helpful in creating this dashboard (along with a number
# of other dashboards within this app.)
import dash
from dash import html, dcc, callback, Output, Input
from data_import import df_curr_enrollment
import plotly.express as px
import dash_bootstrap_components as dbc
dash.register_page(__name__, path='/simple_interactive_dashboard')
# Configuring the page's layout:
layout = dbc.Container([
dbc.Row(dbc.Col(dcc.Markdown('''
# Simple Interactive Enrollment Dashboard
This dashboard provides a relatively simple overview of NVCU
enrollment. There are five preset comparison options and two preset
filter options, allowing the user to create a variety of custom views.
This dashboard is not as versatile or flexible as those that apply the
Dash-Pivottable library (e.g. the 'Dash Pivottable (Survey Results)'
dashboard), but it can still serve as a helpful introduction
to some of Dash's core features.
This dashboard also applies the Dash Bootstrap Components (
https://dash-bootstrap-components.opensource.faculty.ai)
library in order
to create a more condensed layout that accommodates a range of
screen sizes. This library is also used in certain other dashboards
within this app.
'''), lg=9)),
dbc.Row([
dbc.Col(html.H5("Comparison Options:"), lg=3),
dbc.Col(dcc.Dropdown(['College', 'Level',
'College and Level', 'Level and College',
'All Students'], 'College and Level',
id='simple_enrollment_index'), lg=2)
]),
dbc.Row([
dbc.Col(html.H5("College Filter:"), lg=3),
dbc.Col(
dcc.Dropdown(df_curr_enrollment['College'].unique(),
df_curr_enrollment['College'].unique(),
multi=True,
id='college_filter'), lg=3),
]),
dbc.Row([
dbc.Col(html.H5("Level Filter:"), lg=3),
dbc.Col(dcc.Dropdown(df_curr_enrollment['Level'].unique(),
df_curr_enrollment['Level'].unique(),
multi=True,
id='level_filter'), lg=3)
]),
# For more information about the multi-dropdown option,
# see https://dash.plotly.com/dash-core-components/dropdown
dcc.Graph(id='simple_enrollment_view')])
# Configuring a callback that can convert the index and filter options
# specified by the user into a custom chart:
@callback(
Output('simple_enrollment_view', 'figure'),
Input('simple_enrollment_index', 'value'),
Input('college_filter', 'value'),
Input('level_filter', 'value')
)
# The following function uses the Input values specified above
# to update the chart shown within this page.
def display_graph(pivot_index, college_filter, level_filter):
# Filtering a copy of df_curr_enrollment to include only the filter
# values specified by the user:
# print(college_filter, level_filter)
df_curr_enrollment_for_chart = df_curr_enrollment.copy().query(
"College in @college_filter & Level in @level_filter")
# Using the pivot_index argument to determine which values
# to pass to the pd.pivot_table() and px.bar() calls within
# this function:
if pivot_index == 'College':
index = 'College'
x = 'College'
color = 'College'
barmode = 'relative'
elif pivot_index == 'Level':
index = ['Level For Sorting', 'Level']
x = 'Level'
color = 'Level'
barmode = 'relative'
elif pivot_index == 'College and Level':
index = ['College', 'Level For Sorting', 'Level']
x = 'College'
color = 'Level'
barmode = 'group'
elif pivot_index == 'Level and College':
index = ['Level For Sorting', 'Level', 'College']
x = 'Level'
color = 'College'
barmode = 'group'
elif pivot_index == 'All Students': # In this case, all data will
# be grouped together. We'll create a new column named
# 'All Students' that can serve as the x axis label for this data.
df_curr_enrollment_for_chart['All Students'] = 'All Students'
index = 'All Students'
color = 'All Students'
x = 'All Students'
barmode = 'relative'
# Creating a pivot table that can serve as the basis for our
# enrollment chart:
df_simple_enrollment_pivot = df_curr_enrollment_for_chart.pivot_table(
index=index,
values='Enrollment', aggfunc='sum').reset_index()
# Creating this chart:
fig_simple_enrollment = px.bar(df_simple_enrollment_pivot,
x=x, y='Enrollment', color=color,
text_auto='.0f', barmode=barmode,
title=f'NVCU Enrollment by {pivot_index}')
return fig_simple_enrollment
login.html#
<!DOCTYPE html>
<!-- Source: jinnyzor at https://community.plotly.com/t/dash-app-pages-
with-flask-login-flow-using-flask/69507/38. (I made some minor
additions to this code.) Jinnyzor wrote later in this
thread that "this is free to use, no license". -->
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Flask Login Flow</title>
</head>
<body>
<form method="POST" action="/login">
<div>Please log in to continue:</div>
<div>Note: this login page is simply meant to demonstrate that Dash
apps can be used with the Flask-Login library. No private
information is being
displayed.<br>You can use <strong>hello</strong> as the username
and <strong>world</strong> as the password.</div>
<div><span style="color:red">{{message}}</span></div>
<input placeholder="Enter your username here" type="text"
id="uname-box" name='username'>
<input placeholder="Enter your password here" type="password"
id="pwd-box" name='password'>
<Button type="Submit" id="login-button">Log in</Button>
</form>
</body>
</html>
app.py#
# Most of the following code derived from:
# https://community.plotly.com/t/dash-app-pages-
# with-flask-login-flow-using-flask/69507/37
# Note that Nader Elshehabi's code (on which this code was based)
# was released under the MIT license:
# https://github.com/naderelshehabi/dash-flask-login
# I incorporated some additional code from
# https://dash.plotly.com/urls and https://dash-bootstrap-components.
# opensource.faculty.ai/docs/quickstart/ ; in addition, I made
# minor edits to the display text.
"""
CREDIT: This code was originally adapted for Pages based on Nader
Elshehabi's
article:
https://dev.to/naderelshehabi/securing-plotly-dash-using-flask-login-4ia2
https://github.com/naderelshehabi/dash-flask-login
This version was updated by Dash community member @jinnyzor .
For more info, see:
https://community.plotly.com/t/dash-app-pages-with-
flask-login-flow-using-flask/69507
For other Authentication options, see:
Dash Enterprise:
https://dash.plotly.com/authentication#dash-enterprise-auth
Dash Basic Auth:
https://dash.plotly.com/authentication#basic-auth
"""
import os
from flask import Flask, request, redirect, session, jsonify, \
url_for, render_template
from flask_login import login_user, LoginManager, UserMixin, \
logout_user, current_user
import dash
from dash import dcc, html, Input, Output, State, ALL
import dash_bootstrap_components as dbc # See
# https://dash-bootstrap-components.opensource.faculty.ai/docs/quickstart/
# Exposing the Flask Server so that it can be configured for logging in
# Note that this code will be used in place of
# server = app.server , which we used within our Simple_App_Without_Login
# app.
server = Flask(__name__)
@server.before_request
def check_login():
if request.method == 'GET':
if request.path in ['/login', '/logout']:
return
if current_user:
if current_user.is_authenticated:
return
else:
for pg in dash.page_registry:
if request.path == dash.page_registry[pg]['path']:
session['url'] = request.url
return redirect(url_for('login'))
else:
if current_user:
if request.path == '/login' or current_user.is_authenticated:
return
return jsonify(
{'status':'401', 'statusText':'unauthorized access'})
@server.route('/login', methods=['POST', 'GET'])
def login(message=""):
if request.method == 'POST':
if request.form:
username = request.form['username']
password = request.form['password']
if VALID_USERNAME_PASSWORD.get(username) is None:
return """The username and/or password were \
invalid. <a href='/login'>Please try again.</a>"""
if VALID_USERNAME_PASSWORD.get(username) == password:
login_user(User(username))
if 'url' in session:
if session['url']:
url = session['url']
session['url'] = None
return redirect(url) ## redirect to target url
return redirect('/') ## redirect to home
message = "The username and/or password were invalid."
else:
if current_user:
if current_user.is_authenticated:
return redirect('/')
return render_template('login.html', message=message)
@server.route('/logout', methods=['GET'])
def logout():
if current_user:
if current_user.is_authenticated:
logout_user()
return render_template('login.html',
message="You have now been logged out.")
app = dash.Dash(
__name__, server=server, use_pages=True,
suppress_callback_exceptions=True,
external_stylesheets=[dbc.themes.BOOTSTRAP]
)
# See:
# https://dash-bootstrap-components.opensource.faculty.ai/docs/quickstart/
# Keep this out of source code repository - save in a file or a database
# passwords should be encrypted
VALID_USERNAME_PASSWORD = {"test": "test", "hello": "world"}
# Updating the Flask Server configuration with Secret Key to encrypt
# the user session cookie
# server.config.update(SECRET_KEY=os.getenv("SECRET_KEY"))
server.config.update(SECRET_KEY="insecureplaceholder")
# Definitely don't use the above approach in a real-world application!
# Login manager object will be used to login / logout users
login_manager = LoginManager()
login_manager.init_app(server)
login_manager.login_view = "/login"
class User(UserMixin):
# User data model. It has to have at least self.id as a minimum
def __init__(self, username):
self.id = username
@login_manager.user_loader
def load_user(username):
"""This function loads the user by user id. Typically this looks
up the user from a user database.
We won't be registering or looking up users in this example,
since we'll just login using LDAP server.
So we'll simply return a User object with the passed in username.
"""
return User(username)
print(
[(page['name'], page["relative_path"])
for page in dash.page_registry.values()])
# print(dash.page_registry.values())
app.layout = html.Div(
[
html.A('Log out', href='../logout'),
html.Br(),
# The following commented-out and active sections of the script
# show three different ways of building a navigation menu.
# html.H3("Page index:"), # Commented out--see notes below
# The following html.Div() section came from:
# https://dash.plotly.com/urls .
# It places all pages on a separate line, which I found
# to be unwieldy for apps like this one with larger page
# counts.
# html.Div([
# html.Div(
# dcc.Link(f"{page['name']} - {page['path']}",
# href=page["relative_path"])
# ) for page in dash.page_registry.values()
# ]),
# The following variant of the above commented-out code places all
# pages on the same line (though, if the window isn't wide enough,
# some pages may get placed on separate lines). It updates
# automatically to incorporate page additions and deletions,
# but it offers less control over page names and orders than the
# Markdown-based headers that follow it.
# html.Div(
# [dcc.Link(f"{page['name']} | ", href=page["relative_path"])
# for page in dash.page_registry.values()]),
# The following code shows a more manual, Markdown-based
# approach to creating a navigation menu. Although the
# Markdown will need to be updated manually to incorporate
# new pages, this approach allows both page names and orders
# to be easily customized.
# Note that the relative links shown within this example
# suffice for navigation purposes; absolute links aren't
# necessary. (If they were, we might have needed to create
# separate links for offline and online deployments.)
# For documentation on dcc.Markdown,
# visit: https://dash.plotly.com/dash-core-components#markdown
dcc.Markdown('''
### Page Index:
[Home Page](/) |
[Fixed Dashboard](/fixed_dashboard) |
[Simple Interactive Dashboard](/simple_interactive_dashboard)
[Flexible Survey Results Dashboard](/flexible_survey_results_dashboard) |
[Flexible Enrollment Dashboard](/flexible_enrollment_dashboard) |
[Dash Pivottable (Enrollment)](/dash_pivottable_enrollment) |
[Dash Pivottable (Survey Results)](/dash_pivottable_survey_results)
'''),
dash.page_container,
# The following explanatory text will appear at the bottom of
# each page.
dcc.Markdown('''
*This site is part of Python for Nonprofits
(https://github.com/kburchfiel/pfn), created by Kenneth Burchfiel and
licensed under the MIT License.*
*Blessed Carlo Acutis, pray for us!*
'''),
]
)
if __name__ == "__main__":
app.run(debug=True)
auto_pivot_and_graph.py#
# Functions for automatically generating pivot tables, graphs,
# and go.Table objects
# By Kenneth Burchfiel
# Released under the MIT license
# These functions were originally developed within the
# pivot_and_graph_functions.ipynb notebook in the Graphing section
# of Python for Nonprofits.
import plotly.express as px
import plotly.graph_objects as go
from dash import dash_table
def autopivot(df, y, aggfunc, x_vars=[], color=None,
x_vars_to_exclude=[], overall_data_name='All Data',
weight_col=None, filter_tuple_list=[],
convert_x_vars_to_strings=True):
'''
This function will create a pivot table of df that can be used
as the data source for Plotly graphs. It will also return x, y,
color, and barmode variables that can get incorporated within
these graphs. (Storing the charting code within a separate function
makes autopivot() more versatile, as its output can then be used
as the basis for multiple chart types.)
This function has been designed to work with varying lengths of x_vars
(i.e. multiple numbers of x variables), including a length of 0.
It should therefore prove useful for applications, such as interactive
dashboards, that allow the user
to choose an arbitrary number of comparison variables for a graph.
df: the DataFrame to use as the basis for the pivot table.
y: the y variable to use within the Plotly graph.
aggfunc: the aggregate function to use within the pivot_table() call
(e.g. 'mean', 'count', etc.).
x_vars: a list of comparison variables to be converted into
an x variable. Pass an empty list in order to group all y values
together.
color: the variable that should serve as the color argument for the
Plotly graph.
x_vars_to_exclude: variables that will be incorporated into the pivot
table (e.g. to ensure it's sorted correctly) but should not be
present in the final chart.
(For instance, if you've added 'Season' as an x_vars entry or
color variable, and its values are 'Fall', 'Winter', and 'Spring,'
you may also want to add a 'Season For Sorting' column with values
of 0, 1, and 2 for Fall, Winter, and Spring, respectively so that
they'll appear in chronological order within your chart. These
variables should also be placed in x_vars_to_exclude so that they
won't make your final graph more complicated and/or cluttered than
it needs to be.
If a variable within x_vars_to_exclude isn't present in your x_vars
list or your color argument, it will get ignored by the function;
thus, its presence within x_vars_to_exclude shouldn't cause any issues.
overall_data_name: When x_vars is empty, autopivot() will group all
data into a single row. overall_data_name specifies the name that you
would like to give to this data point.
weight_col: A column containing group sizes that, if included, will
be used to calculate weighted averages. (If weight_col is None,
weighted averages will *not* be calculated.)
filter_tuple_list: A list of tuples that allow the DataFrame
to show only a specific set of data. The first item in each tuple
should be a field name, and the second item should be a list of
values to show within that field name.
convert_x_vars_to_strings: if True, the function will convert
all x variables not found in x_vars_to_exclude to strings.
(This can improve the appearance of any graphs that make use of
the pivot table created by this function.)
'''
# Creating a copy of the initial dataset in order to ensure that the
# following code does not modify it:
df_for_pivot = df.copy()
# Filtering the DataFrame based on the contents (if any)
# of filter_tuple_list:
for pair in filter_tuple_list:
df_for_pivot.query(
f"`{pair[0]}` in {pair[1]}", inplace=True)
# Determining which x variables will appear in the final chart:
x_vars_for_chart = list(set(x_vars) - set(x_vars_to_exclude))
# Converting all x variables wthin chart to string form (if
# requested by the caller):
if convert_x_vars_to_strings == True:
for x_var in x_vars_for_chart:
df_for_pivot[x_var] = (
df_for_pivot[x_var].astype('str').copy())
x_var_count = len(x_vars_for_chart)
if weight_col is not None: # In this case, weighted averages
# will be calculated.
# Multiplying each y value by its corresponding weight in order
# to allow weighted averages to be calculated:
df_for_pivot[f'{y}_*_{weight_col}'] = (
df_for_pivot[y] * df_for_pivot[weight_col])
# These 'y_*_weight' values will get added together during the
# pivot table call, as will the corresponding weight column
# values.
if x_var_count == 0: # Because no comparison variables will appear in
# the final chart, the function will instead group all data
# together. It will do so by creating a new column that has the
# same value for each row.
# This column can then serve as the index for both the pivot
# function and the color value.
df_for_pivot[overall_data_name] = overall_data_name
if weight_col is not None: # In this case, a weighted average
# of all data within the table will be created.
df_pivot = df_for_pivot.pivot_table(
index=overall_data_name,
values=[f'{y}_*_{weight_col}', weight_col],
aggfunc='sum').reset_index()
# Calculating the weighted average of y by dividing
# the y_*_weight_col field by its corresponding group size:
df_pivot[y] = (df_pivot[f'{y}_*_{weight_col}']
/ df_pivot[weight_col])
else:
df_pivot = df_for_pivot.pivot_table(
index=overall_data_name, values=y,
aggfunc=aggfunc).reset_index()
x_val_name=overall_data_name
color=overall_data_name
barmode='relative'
index=[overall_data_name] # This value won't be used
# within autopivot() (defined below), but it will still
# get created here in order to prevent scripts that expect it to
# be returned from crashing.
else:
# If the color variable is also present in x_vars and more than
# one variable is present within the set of x vars to be charted*,
# the graph will display redundant data. Therefore,
# the function will remove this variable from x_vars below.
# (If the only x variable to be charted is also the color
# variable, we won't want to remove it from x_vars; otherwise,
# we'd end up with an empty list of x variables.)
# *This set (defined above as x_vars_for_chart) excludes any
# variables also present in x_vars_to_exclude. This will prevent
# the function from removing a color variable from x_vars that
# would have been the only variable left in x_vars following the
# removal of the variable to exclude (which would cause the
# function to crash).
# (For example: suppose our color variable were 'Season';
# our x_vars contents were ['Season', 'Season_for_Sorting'];
# and our x_vars_to_exclude list were ['Season_for_Sorting']. If
# we chose to remove the color variable from x_vars
# because it contained more than one variable, we'd end up with
# an empty x_vars list once 'Season_for_Sorting' got removed.
# Removing this variable to exclude from our list of x vars to
# pass to len() below will prevent this error.
if (color in x_vars) and (len(x_vars_for_chart) > 1):
x_vars.remove(color)
print("x_vars:",x_vars)
# Initializing a list of variables to be passed to the 'index'
# argument within the pivot_table call:
index = x_vars.copy()
# We'll want to make sure to include the color variable in the
# pivot index as well so that it can be accessed by the
# charting code.
if color is not None and color not in index:
index.append(color)
print("index prior to pivot_table() call:", index)
if weight_col is not None: # In this case, weighted averages
# will be calculated.
df_pivot = df_for_pivot.pivot_table(
index=index, values=[
f'{y}_*_{weight_col}', weight_col],
aggfunc='sum').reset_index()
df_pivot[y] = (
df_pivot[f'{y}_*_{weight_col}'] / df_pivot[weight_col])
else:
df_pivot = df_for_pivot.pivot_table(
index=index, values=y,
aggfunc=aggfunc).reset_index()
# Now that the pivot table has been created, the variables
# in x_vars_to_exclude can be removed from our x_vars list, our
# index, and our DataFrame, thus preventing our final
# graphs from being too cluttered.
for var_to_exclude in x_vars_to_exclude:
if var_to_exclude in x_vars:
x_vars.remove(var_to_exclude)
index.remove(var_to_exclude)
# These variables could get removed from df_pivot also,
# but the user might find them helpful for future sorting
# needs-- so they'll be retained for now.
# df_pivot.drop(var_to_exclude, axis = 1, inplace = True)
# Determining the name of the x value column by
# joining together all of the values in x_vars that will get
# incorporated into its own values:
x_val_name = ('/').join(x_vars)
# Initializing the x_val_name column values (which will serve
# as the x axis entries within Plotly charts) by
# converting the first item within x_vars to a string,
# then adding other string-formatted values to it:
# (Slashes will separate these various values.)
df_pivot[x_val_name] = df_pivot[x_vars[0]].astype('str')
for i in range(1, len(x_vars)):
df_pivot[x_val_name] = (
df_pivot[x_val_name]
+ '/' + df_pivot[x_vars[i]].astype('str'))
# If there are fewer than two unique variables to be graphed,
# we'll want to set the barmode argument to 'relative' so that,
# in the event we create a bar chart, the bars won't be far
# apart from one another. If there are
# two or more unique variables, we can use 'group' instead.
# (If there's a single variable within x_vars that matches
# the 'color' variable, we will also want to set barmode to
# 'relative,' since only
# one variable will actually be displayed on the graph. Therefore,
# I added in a set() call in the following code; set() keeps only
# unique instances of a list and will thus prevent the same
# x_vars variable and color variable from being counted as two
# variables within this if/else statement.
if color is not None:
unique_graphed_vars = set(x_vars + [color])
else:
unique_graphed_vars = set(x_vars)
if len(unique_graphed_vars) < 2:
barmode = 'relative'
else:
barmode = 'group'
return df_pivot, x_val_name, y, color, barmode, x_var_count, \
index, aggfunc
def autobar(df_pivot, x_val_name, y, color, barmode, x_var_count,
index, aggfunc, custom_aggfunc_name=None,
text_auto='.2f'):
'''This function creates a bar graph of a pivot table (such as one
created within autopivot()).
Most arguments for this function
correspond to the values returned by autopivot(); more information on
those arguments can be found within that function.
custom_aggfunc_name: A string to use in place of the aggregate
function name in the chart title. (The general chart title format
created by this code will be (aggfunc + y + 'by' + 'x_val_name'.
if custom_aggfunc_name is present, it will take the place of
aggfunc within the title. Note that '' can be passed to
this parameter in order to exclude the aggregate function
from chart titles.)
text_auto: The value to pass to the text_auto argument of px.bar(),
which specifies whether (and how) to display data labels on bars.
Set this variable to False if you wish not to include any labels;
set to '.2f' to show labels up to 2 decimal points; set to '.0f'
to show labels as integers; and '.1%' to show labels in percentage
form with a single decimal point. These are just some of the many
options you can pass for this argument.
'''
# Creating a title for the chart:
# Suppose our y value is 'Score' and our aggregate function is 'mean.'
# If our original x variable count was 0, our title can simply be
# 'Overall Mean Score.' If we had just one graph variable ('College')
# and no color variable, our title could be 'Mean Score by College.'
# If we also had a 'Level' color variable, our title
# could be 'Mean Score by College and Level'. Finally, if we added
# another x variable ('Season' to our list, our mean title could be
# 'Mean Score by College, Season, and Level.'
# The following code includes four title definitions to cover
# these four scenarios. (Note that 'index' is used rather than
# 'x_vars' because the former variable includes both our x variables
# and (if present) our color variable, and both of these should be
# incorporated into the title.)
# Determining how to represent the aggregate function within
# titles:
# Since these function names will immediately precede the chart's
# y value some names will fit better than others. 'mean' and
# 'median' can precede y value names wihout any trouble; however,
# 'sum' and 'count' can't. (For example, 'Mean Score' works fine,
# but 'Sum Students' or 'Count Students' isn't gramatically correct.)
# Therefore, the following code replaces 'count' and 'sum' within
# titles with 'Total'; other aggregate function names are left
# in place.
if custom_aggfunc_name is not None:
aggfunc_name = custom_aggfunc_name
else:
if aggfunc in ['count', 'sum']:
aggfunc_name = 'Total'
else:
aggfunc_name = aggfunc
if x_var_count == 0:
# The caller may have set aggfunc_name to '' in order to
# exclude the aggregate function from display. In this case,
# aggfunc_name shouldn't be included within the plot_title
# definition, as doing so would add an extra space
# to the title.
if len(aggfunc_name) == 0:
plot_title = f"Overall {y}"
else:
plot_title = f"Overall {aggfunc_name.title()} {y}"
elif len(index) == 1:
plot_title = f"{aggfunc_name.title()} {y} by {index[0]}"
elif len(index) == 2:
plot_title = f"{aggfunc_name.title()} {y} \
by {index[0]} and {index[1]}"
else:
plot_title = f"{aggfunc_name.title()} {y} by {(', ').join(
index[0:-1])}, and {index[-1]}"
if len(df_pivot) > 0:
# The following code will still work if color is set to None.
fig = px.bar(df_pivot, x = x_val_name, y = y,
color=color, barmode=barmode,
text_auto=text_auto, title=plot_title)
else: # In this case, there's no data to plot,
# so an empty figure will be returned instead.
fig = px.bar(title=plot_title)
if x_var_count == 0: # In this case, the x axis tick, x axis title,
# and legend entry will all be the same, so we can hide two
# of those elements.
fig.update_layout(showlegend=False,
xaxis_title=None)
return fig
def autotable(df_pivot):
'''This function converts a DataFrame into a Graph Objects-based
Table.'''
# For more details about go.Table objects, see:
# https://plotly.com/python/table/
# The following code was based on:
# https://plotly.com/python/table/#use-a-pandas-dataframe
table = go.Figure(data=[go.Table(
header=dict(values=list(df_pivot.columns),
fill_color='lightgray',
align='left'),
cells=dict(values=[df_pivot[column]
for column in df_pivot.columns],
fill_color='white',
align='left'))
])
return table
def autopivot_plus_bar(
df, y, aggfunc, x_vars=[], color=None,
x_vars_to_exclude=[], overall_data_name='All Data',
weight_col=None, filter_tuple_list=[],
custom_aggfunc_name=None, convert_x_vars_to_strings=True,
create_table=False, text_auto='.2f'):
'''This function calls both autopivot() and autobar(), thus
simplifying the process of using both functions within a script.
Almost all of the variables within this function correspond to
autopivot() and autobar() variables with the same name; consult
those functions' definitions for explanations of them.
create_table: set to True to return a table along with the bar
graph.'''
df_pivot, x_val_name, y, color, barmode, x_var_count, \
index, aggfunc=autopivot(
df=df, y=y, aggfunc=aggfunc,
x_vars=x_vars, color=color,
x_vars_to_exclude=x_vars_to_exclude,
overall_data_name=overall_data_name, weight_col=weight_col,
filter_tuple_list=filter_tuple_list,
convert_x_vars_to_strings=convert_x_vars_to_strings)
fig_bar = autobar(
df_pivot=df_pivot, x_val_name=x_val_name, y=y,
color=color, barmode=barmode, x_var_count=x_var_count,
index=index, aggfunc=aggfunc,
custom_aggfunc_name=custom_aggfunc_name,
text_auto=text_auto)
if create_table == False:
return fig_bar
else: # In this case, a tabular view of df_pivot will get
# created via autotable(), after which the function will
# return both fig_bar and this tabular view.
table = autotable(df_pivot)
return fig_bar, table
data_import.py#
# Data import script:
# This script will import datasets that will be used by multiple pages.
# Taking care of these imports here *may* make the site more efficient
# by reducing the number of times each dataset needs to be imported.
offline_import = False # Allows for source data to get read in locally,
# which may improve performance when debugging or editing the code.
# (This should be set to False prior to deploying this app online.)
import pandas as pd
def improve_col_display(df):
'''This function replaces underscores in column names with spaces
and also converts them to title space, thus improving their
appearance within chart titles.
Since some values (e.g. 'ID') are best capitalized rather than
converted to title case, the function also includes a
df.rename() call. This can be expanded as needed to address any other
issues caused by the blanket title case conversion that precedes it.
'''
df.columns = [column.replace('_', ' ').title()
for column in df.columns]
df.rename(
columns = {'Student Id':'Student ID'}, inplace = True)
if offline_import == True: # In this case, the source data will get
# imported from a local file.
print("Importing source data from local .csv files.")
df_curr_enrollment = pd.read_csv(
'../../Appendix/curr_enrollment.csv')
df_survey_results = pd.read_csv(
'../../Appendix/survey_results.csv')
else:
print("Downloading source data from an online source.")
df_curr_enrollment = pd.read_csv(
'https://raw.githubusercontent.com/kburchfiel/\
pfn/main/Appendix/curr_enrollment.csv')
df_survey_results = pd.read_csv('https://raw.githubusercontent.com/\
kburchfiel/pfn/main/Appendix/survey_results.csv')
print("Imported current enrollment data.") # Allows us to check how many
# times this data will get imported during the web app's operation
print("Imported survey results.")
# Adding an 'Enrollment' column (which will be useful for pivot tables)
# and chart titles:
improve_col_display(df_curr_enrollment)
df_curr_enrollment['Enrollment'] = 1
improve_col_display(df_survey_results)
df_survey_results['Count'] = 1
# Merging our survey and enrollment data together in order to allow
# survey results to be compared by college, level, etc.:
df_survey_results_extra_data = df_survey_results.merge(
df_curr_enrollment, on = 'Student ID', how = 'left')[
['Starting Year', 'Season', 'Score', 'Gender', 'Matriculation Year',
'College', 'Class Of', 'Level', 'Level For Sorting']]
print("Merged enrollment and survey data together to create \
df_survey_results_extra_data.")
import_layout.py#
# import_layout() function definition
# By Kenneth Burchfiel
# Released under the MIT license
import dash
from dash import html, dcc, callback, Output, Input
import dash_bootstrap_components as dbc
def import_layout(df, comparison_list, comparison_default,
color_list, color_default, filter_cols = []):
'''This function generates comparison, color, and filter
components of an interactive dashboard, thus reducing
the amount of code needed to create a multi-dashboard Dash app
and helping ensure consistency across dashboard layouts.
df: the DataFrame from which filter options should be retrieved.
comparison_list and color_list: a list of comparison and color
options to pass to the Comparison Options and Color Option menus,
respectively.
comparison_default and color_default: the default comparison
and color options for comparison_list and color_list, respectively.
filter_cols: The columns in df for which to create filter dropdowns.
'''
df_layout = df.copy() # This step ensures that this function
# will not make any changes to the original DataFrame.
# Initializing the layout that will be returned to
# the Dash app code:
# This layout will include rows for comparisons and color options.
layout = [dbc.Row([
dbc.Col(html.H5("Comparison Options:"), lg = 2),
dbc.Col(dcc.Dropdown(
comparison_list,
comparison_default, multi = True,
id = 'comparison_options'), lg = 3),
dbc.Col(html.H5("Color Option:"), lg = 2),
dbc.Col(dcc.Dropdown(
color_list, color_default,
id = 'color_option'), lg = 2)])]
# Adding filter rows for each column in filter_cols:
# Note that each filter will get added to the layout
# via an addition operation.
for filter_col in filter_cols:
layout += [dbc.Row([
dbc.Col(html.H5(f"{filter_col} Filter:"), lg = 2),
dbc.Col(
dcc.Dropdown(df_layout[filter_col].unique(),
df_layout[filter_col].unique(),
multi = True,
id = f'{filter_col}_filter'), lg = 3)
])]
# print("Layout:",layout) # May be useful for debugging
return layout
Procfile#
web: gunicorn app:server
requirements.txt#
dash==3.0.0
dash-bootstrap-components==2.0.0
dash-pivottable==0.0.2
Flask==3.0.3
Flask-Login==0.6.3
gunicorn==23.0.0
pandas==2.2.3
plotly==6.0.1
Werkzeug==3.0.6
Conclusion#
As this code demonstrates, it’s more complicated to set up a Dash app than it is to simply send a static copy of a chart or a local .csv file to a colleague. However, as you gain more experience with Dash, I believe you’ll find that the extra work involved in creating these websites is well worth it.