Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Major refactor, bug fixes, and update dep versions for v0.4.0 #5

Merged
merged 15 commits into from
Mar 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 9 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,13 @@
## Overview

Quickboard is a collection of Python classes and utilities for making scalable dashboards. Built on top of
[Dash](https://github.com/plotly/dash) and [Plotly](https://github.com/plotly/plotly.py), Quickboard provides
an assortment of tools and pre-made components to mix and match, achieving a balance between ease-of-use and
customizability.
[Dash](https://github.com/plotly/dash) and [Plotly](https://github.com/plotly/plotly.py), Quickboard provides an assortment of tools and pre-made components to mix and match,
achieving a balance between ease-of-use and customizability.

All visible Quickboard components are instances of `dash.html` objects, so you can fully customize them using knowledge
of the `dash` package. As `plotly` has `plotly.express`, this package can be thought of an (unofficial) incarnation of
an "express" version of `dash`, allowing you to quickly prototype a dashboard, while allowing for full customization
using the usual `dash` API.

The following example was made using Quickboard.

Expand All @@ -17,7 +21,7 @@ The following example was made using Quickboard.
The Quickboard package contains three subpackages of interest for developing dashboards:
* base - the core components used to make the backbone of the dashboard,
* plugins - highly customizable add-ons to augment your other components,
* (EXPERIMENTAL) textboxes - components for having dynamically updated text.
* (DEPRECATED) textboxes - components for having dynamically updated text (to be removed in future version).

More details on using these can be found [below](#usage).

Expand All @@ -40,7 +44,7 @@ plugin interactions, you can use a few of the other Quickboard classes to achiev
Quickboard consists of:
* a **Quickboard** object to hold everything together;
* a (n optional) list of **BaseTab** objects to organize visuals into tabs;
* a **Sidebar** calibrated to hold different *plugins* based on the current tab.
* a **Sidebar** calibrated to hold different **plugins** based on the current tab.

Within each tab, we have
* various **ContentGrid** objects to display other components in a grid, with customizable column wrapping length;
Expand Down
102 changes: 62 additions & 40 deletions docs/beginner_example.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
## Guided Example

> Last Updated: v0.4.0

This is a beginner's guide to creating a larger scale app than the examples shown in the
[Component Gallery](component_gallery.md). To demonstrate how to use the Quickboard components together, let's work
through a simple example. We're going to recreate the following board.

![The finished product](images/beginner_example/guide_final.jpg "Look how short the code at the end is for all of this!")
![The finished product](images/beginner_example/guide_final.png "Look how short the code at the end is for all of this!")

In this guide, we'll run code using a Jupyter notebook. All code blocks should be thought of as cells in the notebook,
though it should also work fine to gather the main pieces into a `.py` file and run it.
Expand All @@ -27,6 +29,8 @@ First we have `data_a.tsv`, generated with:
import numpy as np
import pandas as pd

np.random.seed(0)

data_a = pd.DataFrame({'label': ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm']})
data_a['size'] = np.random.choice(range(1,6), len(data_a))
data_a['weight'] = 5 * np.random.random(len(data_a)) + 2
Expand All @@ -44,45 +48,45 @@ data_b['day'] = np.random.choice(['Mon', 'Wed', 'Fri'], len(data_b))
data_b['batch'] = np.random.choice(['batch1', 'batch2'], len(data_b))
```

Here is what they look like. For `data_a.tsv`:
Here is what they look like. For `data_a.tsv` (generated using `print(data_a.to_markdown())`:

| | label | size | weight | day | batch |
|---:|:--------|-------:|---------:|:------|:--------|
| 0 | a | 2 | 4.04 | Mon | batch2 |
| 1 | b | 3 | 5.64 | Wed | batch2 |
| 2 | c | 1 | 6.61 | Wed | batch2 |
| 3 | d | 3 | 2.54 | Wed | batch1 |
| 4 | e | 5 | 4.29 | Wed | batch2 |
| 5 | f | 3 | 2.1 | Fri | batch2 |
| 6 | g | 2 | 4.83 | Fri | batch1 |
| 7 | h | 5 | 4.79 | Fri | batch1 |
| 8 | i | 1 | 2.54 | Wed | batch1 |
| 9 | j | 5 | 2.46 | Wed | batch1 |
| 10 | k | 3 | 2.15 | Mon | batch1 |
| 11 | l | 5 | 4.5 | Fri | batch1 |
| 12 | m | 3 | 6.34 | Fri | batch2 |
| 0 | a | 5 | 4.39 | Wed | batch1 |
| 1 | b | 1 | 6.06 | Fri | batch1 |
| 2 | c | 4 | 4.4 | Mon | batch1 |
| 3 | d | 4 | 3.96 | Wed | batch2 |
| 4 | e | 4 | 6.18 | Wed | batch2 |
| 5 | f | 2 | 3.69 | Wed | batch1 |
| 6 | g | 4 | 5.24 | Mon | batch1 |
| 7 | h | 3 | 3.84 | Fri | batch1 |
| 8 | i | 5 | 6.79 | Mon | batch2 |
| 9 | j | 1 | 2.7 | Fri | batch2 |
| 10 | k | 1 | 6.35 | Fri | batch1 |
| 11 | l | 5 | 4.37 | Mon | batch2 |
| 12 | m | 3 | 6 | Fri | batch1 |

And for `data_b.tsv`:

| | label | size | measurement | day | batch |
|---:|:--------|-------:|--------------:|:------|:--------|
| 0 | a | 9 | 6.22343 | Wed | batch1 |
| 1 | b | 5 | 7.545 | Mon | batch2 |
| 2 | c | 6 | 5.98645 | Fri | batch1 |
| 3 | d | 7 | 8.30217 | Mon | batch1 |
| 4 | e | 7 | 4.88089 | Wed | batch1 |
| 5 | f | 5 | 9.09543 | Fri | batch1 |
| 6 | g | 8 | 5.43039 | Wed | batch1 |
| 7 | h | 9 | 7.51989 | Mon | batch1 |
| 8 | i | 4 | 7.95468 | Mon | batch2 |
| 9 | j | 6 | 6.99687 | Mon | batch1 |
| 10 | k | 7 | 7.94129 | Wed | batch1 |
| 11 | l | 8 | 7.08559 | Fri | batch2 |
| 12 | m | 9 | 6.02301 | Fri | batch2 |
| 13 | n | 9 | 8.57015 | Wed | batch2 |
| 14 | o | 8 | 4.56614 | Mon | batch2 |
| 15 | p | 6 | 7.72713 | Mon | batch2 |
| 16 | q | 8 | 9.6404 | Wed | batch2 |
| 0 | a | 6 | 7.43992 | Mon | batch1 |
| 1 | b | 4 | 9.71264 | Fri | batch2 |
| 2 | c | 5 | 7.56953 | Mon | batch1 |
| 3 | d | 5 | 7.44541 | Wed | batch2 |
| 4 | e | 5 | 9.9671 | Fri | batch2 |
| 5 | f | 5 | 7.07295 | Fri | batch1 |
| 6 | g | 7 | 5.90058 | Wed | batch1 |
| 7 | h | 7 | 6.32288 | Mon | batch2 |
| 8 | i | 6 | 7.36457 | Wed | batch1 |
| 9 | j | 7 | 5.36605 | Wed | batch2 |
| 10 | k | 4 | 7.72327 | Mon | batch1 |
| 11 | l | 7 | 5.22041 | Fri | batch2 |
| 12 | m | 9 | 8.44842 | Fri | batch1 |
| 13 | n | 8 | 9.647 | Fri | batch2 |
| 14 | o | 5 | 7.63769 | Fri | batch1 |
| 15 | p | 6 | 3.095 | Wed | batch2 |
| 16 | q | 8 | 7.35992 | Fri | batch1 |

Let's visualize some plots using these data sets.

Expand Down Expand Up @@ -186,7 +190,7 @@ start_app(board, mode="external", port=8050) # Note: The default port is 8050
Running this in Jupyter should output a link to `127.0.0.1:8050`. If you click the link, or go to that url in a browser
after running in a terminal (with the `mode` input removed in that case), you can finally see the board!

![Our first glance at what Quickboard can do!](images/beginner_example/TabA_firstplot.jpg "All tabs are automatically scrollable!")
![Our first glance at what Quickboard can do!](images/beginner_example/TabA_firstplot.png "All tabs are automatically scrollable!")

There it is! But it's still a lot of overhead for just one plot. Let's try adding some more stuff.

Expand Down Expand Up @@ -226,7 +230,7 @@ Rerun this code block and the ones following it, and run the `start_app` functio
(Tip: If you get `Duplicate callback outputs` errors when trying to modify and rerun, try restarting the notebook
kernel.)

![That looks better!](images/beginner_example/TabA_secondplot.jpg "That was easy!")
![That looks better!](images/beginner_example/TabA_secondplot.png "That was easy!")

This example should show some of the power of building up a board with modular components like this. An investment in
the higher layer objects makes it very easy to drop in new features. The new hierarchy looks like this:
Expand Down Expand Up @@ -280,14 +284,17 @@ TabA = qbb.BaseTab(

Now when you rerun the code, you'll see our plugin on the left.

![Now we've got our first plugin!](images/beginner_example/TabA_sidebarplugin.jpg "Unlimited power!")
![Now we've got our first plugin!](images/beginner_example/TabA_sidebarplugin.png "Unlimited power!")

Now our board is interactive beyond the usual Plotly capabilities. Try clicking the checklist buttons and watch the
plots update in real time! Can you see what's happening?

As the name implies, the `DataFilterChecklist` plugin filters the data so that the `day` column is contained in the list
of selected values. Because we made it a sidebar plugin, this affects all of the plots on the page.

Note the checklist plugin comes with the "All" or "None" toggles for free. This can be turned off using the
`toggle_all_button` input to the plugin constructor.

#### Plot Plugins

But what if we wanted to do a similar style of filtering just on one specific plot? We can use a similar syntax but
Expand Down Expand Up @@ -320,7 +327,7 @@ You can rerun the code and see what appears. There's now a similar checklist bel
filtering the data, but just for that plot. If you restrict on both the sidebar and the plot plugins, you'll get the
intersection of both control effects. You can add a similar control to the other plot to make it look a little nicer.

![Added some plot plugins!](images/beginner_example/TabA_plotplugins.jpg "Not enough days in the week...")
![Added some plot plugins!](images/beginner_example/TabA_plotplugins.png "Not enough days in the week...")

Our hierarchy now looks like:
```
Expand Down Expand Up @@ -371,7 +378,7 @@ LabelBarPlot = qbb.PlotPanel(

If the code looks like it's getting a little messy, we can always refactor with something like:
```
myplg = plg.DataFilterChecklist(...) # But make sure all of the id's are valid!
myplg = plg.DataFilterChecklist(...)
...
plugins=[
myplg, ...
Expand All @@ -381,7 +388,7 @@ myplg = plg.DataFilterChecklist(...) # But make sure all of the id's are vali

Let's add it to our code and see what we get.

![We've got radio buttons!](images/beginner_example/TabA_radioplugin.jpg "How many possible plots can we make now by just clicking?")
![We've got radio buttons!](images/beginner_example/TabA_radioplugin.png "How many possible plots can we make now by just clicking?")

Now we can toggle between seeing `size` and `weight` on the y-axis with the click of a button!

Expand Down Expand Up @@ -436,7 +443,7 @@ Here is what the hierarchy looks like at the very end:
| ├─ BaseTab (B)
```

Here is the final code in full:
Here is the final code in full you can run from start to finish:
```
import plotly.express as px

Expand All @@ -445,6 +452,21 @@ import quickboard.plugins as plg
from quickboard.app import start_app


np.random.seed(0)

data_a = pd.DataFrame({'label': ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm']})
data_a['size'] = np.random.choice(range(1,6), len(data_a))
data_a['weight'] = 5 * np.random.random(len(data_a)) + 2
data_a['weight'] = data_a['weight'].apply(lambda x: round(x, 2))
data_a['day'] = np.random.choice(['Mon', 'Wed', 'Fri'], len(data_a))
data_a['batch'] = np.random.choice(['batch1', 'batch2'], len(data_a))

data_b = pd.DataFrame({'label': ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q']})
data_b['size'] = np.random.choice(range(4,10), len(data_b))
data_b['measurement'] = 7 * np.random.random(len(data_b)) + 3
data_b['day'] = np.random.choice(['Mon', 'Wed', 'Fri'], len(data_b))
data_b['batch'] = np.random.choice(['batch1', 'batch2'], len(data_b))

SizeWeightPlot = qbb.PlotPanel(
header='My Demo Scatterplot',
plotter=px.scatter,
Expand Down Expand Up @@ -528,5 +550,5 @@ board = qbb.Quickboard(
]
)

start_app(board, mode="external", port=8050) # Note: Remove 'mode' input if running a .py file instead of notebook
start_app(board, jupyter_mode="external", port=8050)
```
52 changes: 47 additions & 5 deletions docs/component_gallery.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,11 @@ Tip: If you forget the inputs and usage of a particular class, use the `help` co
2. [DataPanel](#datapanel)
2. [Plugins](#plugins)
1. [DataFilterRadioButtons](#datafilterradiobuttons)
2. [DataFilterChecklist](#datafilterchecklist)
3. [DataFilterSlider](#datafilterslider)
4. [DataFilterRangeSlider](#datafilterrangeslider)
5. [PlotInputRadioButtons](#plotinputradiobuttons)
2. [DataFilterDropdown](#datafilterdropdown)
3. [DataFilterChecklist](#datafilterchecklist)
4. [DataFilterSlider](#datafilterslider)
5. [DataFilterRangeSlider](#datafilterrangeslider)
6. [PlotInputRadioButtons](#plotinputradiobuttons)
3. [Interactive Plot Data](#interactive-plot-data)
1. [Hover/Click Data Plot](#hover--click-data-plot)
2. [Selected Data Plot](#selected-data-plot)
Expand Down Expand Up @@ -187,6 +188,46 @@ Simply add `'range_y': [50, 85]` to the `plot_inputs` dict.

---

### DataFilterDropdown

#### Demo

![DataFilterDropdown](images/gallery/DataFilterDropdown_demo.gif "Like radio buttons but dropdown.")

#### Code

```
my_plot = qbb.PlotPanel(
plotter=px.scatter,
plot_inputs={
'x': 'year',
'y': 'lifeExp',
'title': "Life Expectancy over the Years for Selected Country"
},
data_source=df,
plugins=[
plg.DataFilterDropdown(
header="Select a country",
data_col="country",
data_values=["United States", "Canada", "Mexico"]
)
]
)

start_app(my_plot, mode="external", port=8055)
```

#### Explanation

This works identically to the `DataFilterRadioButton` example above, but using a Dropdown component instead of Radio
Buttons to make the selection.

#### Tips & Tricks

Same as the `DataFilterRadioButton` example above.

---

### DataFilterChecklist

#### Demo
Expand All @@ -213,7 +254,8 @@ my_plot = qbb.PlotPanel(
plg.DataFilterChecklist(
header="Select countries to include",
data_col="country",
data_values=countries
data_values=countries,
toggle_all_button=False # Switch to True (default) for All/None toggle buttons
)
]
)
Expand Down
Binary file removed docs/images/beginner_example/TabA_firstplot.jpg
Binary file not shown.
Binary file added docs/images/beginner_example/TabA_firstplot.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file removed docs/images/beginner_example/TabA_plotplugins.jpg
Binary file not shown.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file removed docs/images/beginner_example/TabA_radioplugin.jpg
Binary file not shown.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file removed docs/images/beginner_example/TabA_secondplot.jpg
Binary file not shown.
Binary file added docs/images/beginner_example/TabA_secondplot.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file removed docs/images/beginner_example/TabA_sidebarplugin.jpg
Binary file not shown.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file removed docs/images/beginner_example/guide_final.jpg
Binary file not shown.
Binary file added docs/images/beginner_example/guide_final.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/images/beginner_example/guide_firstplot.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/images/gallery/DataFilterDropdown_demo.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
40 changes: 20 additions & 20 deletions quickboard/app.py
Original file line number Diff line number Diff line change
@@ -1,45 +1,45 @@
import os

import dash
import dash_bootstrap_components as dbc
from dash import html
from dash import dcc

from quickboard.dashsetup import app
from quickboard.utils.environment import isnotebook


def generate_layout(board):
"""
Creates the layout of the app using a Quickboard object.
"""
layout = html.Div([
board.container,
board,
dcc.Store(id='data_store', data={'current_tab': "", 'sidebar_controls': []}),
])

return layout

def create_app(board, theme=dbc.themes.BOOTSTRAP, app_title="Dash"):
app = dash.Dash(__name__, external_stylesheets=[theme], title=app_title)
app.config.suppress_callback_exceptions = True

def start_app(board, mode='external', host=os.getenv("HOST", "127.0.0.1"), port=8050, proxy=None, debug=True,
app_title="Dash", **flask):
"""
Takes an app instance and configures its board layout, then runs the app on given port. Extra args get sent to
Flask server.
"""
app.title = app_title
app.layout = generate_layout(board)
return app


if isnotebook():
app.run_server(mode=mode, host=host, port=port, proxy=proxy, debug=debug, **flask)
else:
app.run(host=host, port=port, proxy=proxy, debug=debug, **flask)
def start_app(board, theme=dbc.themes.BOOTSTRAP, jupyter_mode='external', host=os.getenv("HOST", "127.0.0.1"),
port=8050, proxy=None, debug=True, app_title="Dash", **flask):
"""
Takes a Quickboard object and creates app with layout, then runs the app on given port.
Extra args get sent to Flask server. Theme should be selected from dbc.themes.
Other nice themes: DARKLY, CYBORG, BOOTSTRAP, FLATLY, LUX, LUMEN, SOLAR.
"""
app = create_app(board=board, theme=theme, app_title=app_title)
app.run(host=host, jupyter_mode=jupyter_mode, port=port, proxy=proxy, debug=debug, **flask)


def deploy_app(board):
def get_app_server(board, theme=dbc.themes.BOOTSTRAP, app_title="Dash"):
"""
This method can be used as an alternative to the above for running the app in a production environment, e.g. with
gunicorn, using the server variable.
"""
app.layout = generate_layout(board)

global server
server = app.server
app = create_app(board=board, theme=theme, app_title=app_title)
return app.server
Loading
Loading