├── .gitignore ├── LICENSE ├── README.md ├── part1 ├── README.md ├── assets │ └── style.css ├── data │ └── transactions.csv ├── environment │ ├── README.md │ ├── conda.yaml │ ├── requirements-dev.txt │ └── requirements.txt ├── main.py └── src │ ├── __init__.py │ └── components │ ├── __init__.py │ ├── bar_chart.py │ ├── ids.py │ ├── layout.py │ └── nation_dropdown.py ├── part2 ├── README.md ├── assets │ └── style.css ├── data │ └── transactions.csv ├── environment │ ├── README.md │ ├── conda.yaml │ ├── requirements-dev.txt │ └── requirements.txt ├── main.py └── src │ ├── __init__.py │ ├── components │ ├── __init__.py │ ├── bar_chart.py │ ├── category_dropdown.py │ ├── ids.py │ ├── layout.py │ ├── month_dropdown.py │ ├── pie_chart.py │ └── year_dropdown.py │ └── data │ ├── __init__.py │ └── loader.py └── part3 ├── README.md ├── assets └── style.css ├── data └── transactions.csv ├── environment ├── README.md ├── conda.yaml ├── requirements-dev.txt └── requirements.txt ├── locale ├── category.en.yml ├── category.nl.yml ├── general.en.yml └── general.nl.yml ├── main.py └── src ├── __init__.py ├── components ├── __init__.py ├── bar_chart.py ├── category_dropdown.py ├── dropdown_helper.py ├── ids.py ├── layout.py ├── month_dropdown.py ├── pie_chart.py └── year_dropdown.py └── data ├── __init__.py ├── loader.py └── source.py /.gitignore: -------------------------------------------------------------------------------- 1 | **/*.pyc 2 | .coverage 3 | htmlcov 4 | **/.DS_Store 5 | **/*.log -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 ArjanCodes and Mark Todisco 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Dash tutorial series 2 | 3 | In this video series, I show you how to set up data visualization using Plotly Dash in Python in a few easy steps. This is a three-part miniseries. In part 1, I show the basics of Dash. In part 2 I dive more deeply into loading, filtering and displaying data. And finally in part 3, I cover a few ways to improve the design and make the application more ready for production. 4 | 5 | Video part 1: https://youtu.be/XOFrvzWFM7Y. 6 | 7 | Video part 2: https://youtu.be/GlRauKqI08Y. 8 | 9 | Video part 3: https://youtu.be/L_KlPZ5qBOU. 10 | -------------------------------------------------------------------------------- /part1/README.md: -------------------------------------------------------------------------------- 1 | # Dash App Tutorial 2 | 3 | ## Running the app 4 | 5 | Run the following commands and open the local host web address chosen by Dash. 6 | 7 | ```shell 8 | python ./main.py 9 | ``` 10 | 11 | An example of the expected terminal messages are shown below: 12 | 13 | ```shell 14 | Dash is running on http://127.0.0.1:8050/ 15 | 16 | * Serving Flask app 'app' (lazy loading) 17 | * Environment: production 18 | WARNING: This is a development server. Do not use it in a production deployment. 19 | Use a production WSGI server instead. 20 | * Debug mode: on 21 | ``` 22 | -------------------------------------------------------------------------------- /part1/assets/style.css: -------------------------------------------------------------------------------- 1 | hr { 2 | border-top: 2px solid gray; 3 | } 4 | 5 | .dropdown-container { 6 | display: grid; 7 | grid-template-columns: repeat(3, 1fr); 8 | gap: 10px; 9 | } 10 | 11 | .dropdown-button { 12 | width: 100%; 13 | margin-top: 5px; 14 | } 15 | 16 | .tabs-group { 17 | padding-top: 20px; 18 | } 19 | 20 | .app-div { 21 | padding: 10px; 22 | } 23 | 24 | .data-table-div { 25 | margin: auto; 26 | padding: 10px; 27 | } 28 | 29 | .budget-label { 30 | font-size: 20px; 31 | font-weight: bold; 32 | } 33 | 34 | .budget-text { 35 | font-size: 20px; 36 | } 37 | 38 | .budget-text-value { 39 | font-size: 20px; 40 | font-style: italic; 41 | } 42 | 43 | .budget-progress-bar { 44 | height: 20px; 45 | } 46 | 47 | .budget-tooltip { 48 | font-size: 18px; 49 | } 50 | 51 | .checklist { 52 | text-align: center; 53 | padding-top: 10px; 54 | padding-bottom: 20px; 55 | } 56 | 57 | @media(max-width: 800px) { 58 | .dropdown-container { 59 | grid-template-columns: 1fr; 60 | } 61 | } -------------------------------------------------------------------------------- /part1/data/transactions.csv: -------------------------------------------------------------------------------- 1 | date,amount,category 2 | 2020-01-01,89.92,groceries 3 | 2020-01-01,48.74,entertainment 4 | 2020-01-01,39.08,groceries 5 | 2020-01-01,48.36,groceries 6 | 2020-01-01,77.36,groceries 7 | 2020-01-01,73.46,groceries 8 | 2020-01-01,57.16,groceries 9 | 2020-01-01,75.94,groceries 10 | 2020-02-01,76.29,healthcare 11 | 2020-02-01,16.09,entertainment 12 | 2020-02-01,82.62,groceries 13 | 2020-02-01,21.61,utilities 14 | 2020-02-01,43.55,entertainment 15 | 2020-02-01,28.57,car 16 | 2020-02-01,86.28,groceries 17 | 2020-02-01,43.51,housing 18 | 2020-02-01,7.93,car 19 | 2020-03-01,51.83,groceries 20 | 2020-03-01,48.8,travel 21 | 2020-03-01,30.33,groceries 22 | 2020-03-01,85.14,groceries 23 | 2020-03-01,96.06,groceries 24 | 2020-03-01,66.16,housing 25 | 2020-03-01,53.73,groceries 26 | 2020-03-01,64.25,healthcare 27 | 2020-03-01,94.17,groceries 28 | 2020-04-01,65.61,housing 29 | 2020-04-01,11.49,travel 30 | 2020-04-01,88.06,groceries 31 | 2020-04-01,58.63,groceries 32 | 2020-04-01,28.14,groceries 33 | 2020-04-01,32.49,utilities 34 | 2020-04-01,6.55,groceries 35 | 2020-04-01,94.11,groceries 36 | 2020-05-01,1.19,groceries 37 | 2020-05-01,56.66,groceries 38 | 2020-05-01,49.05,entertainment 39 | 2020-05-01,41.46,healthcare 40 | 2020-05-01,71.48,entertainment 41 | 2020-05-01,32.84,groceries 42 | 2020-05-01,78.47,entertainment 43 | 2020-05-01,29.33,groceries 44 | 2020-06-01,87.67,groceries 45 | 2020-06-01,44.3,housing 46 | 2020-06-01,56.0,entertainment 47 | 2020-06-01,66.25,travel 48 | 2020-06-01,97.5,healthcare 49 | 2020-06-01,29.02,groceries 50 | 2020-06-01,97.86,groceries 51 | 2020-07-01,47.77,housing 52 | 2020-07-01,80.89,groceries 53 | 2020-07-01,21.09,utilities 54 | 2020-07-01,4.62,car 55 | 2020-07-01,96.13,utilities 56 | 2020-07-01,12.53,groceries 57 | 2020-07-01,35.77,entertainment 58 | 2020-08-01,82.73,healthcare 59 | 2020-08-01,47.77,groceries 60 | 2020-08-01,74.13,groceries 61 | 2020-08-01,38.33,entertainment 62 | 2020-08-01,65.55,housing 63 | 2020-08-01,6.29,groceries 64 | 2020-08-01,25.23,groceries 65 | 2020-09-01,49.51,travel 66 | 2020-09-01,17.92,entertainment 67 | 2020-09-01,74.69,travel 68 | 2020-09-01,89.4,groceries 69 | 2020-09-01,55.0,travel 70 | 2020-09-01,81.93,utilities 71 | 2020-09-01,57.69,healthcare 72 | 2020-09-01,22.22,entertainment 73 | 2020-09-01,13.22,entertainment 74 | 2020-09-01,77.76,groceries 75 | 2020-09-01,13.22,groceries 76 | 2020-10-01,57.58,groceries 77 | 2020-10-01,4.0,car 78 | 2020-10-01,20.48,utilities 79 | 2020-10-01,15.74,utilities 80 | 2020-10-01,59.31,entertainment 81 | 2020-10-01,15.31,groceries 82 | 2020-10-01,96.55,groceries 83 | 2020-10-01,91.65,entertainment 84 | 2020-10-01,50.04,groceries 85 | 2020-10-01,80.78,groceries 86 | 2020-11-01,71.81,groceries 87 | 2020-11-01,24.68,housing 88 | 2020-11-01,69.69,healthcare 89 | 2020-11-01,56.34,groceries 90 | 2020-12-01,66.58,entertainment 91 | 2020-12-01,76.24,utilities 92 | 2020-12-01,81.17,groceries 93 | 2020-12-01,47.01,car 94 | 2020-12-01,71.57,groceries 95 | 2020-12-01,73.81,utilities 96 | 2020-12-01,9.22,healthcare 97 | 2020-12-01,32.22,travel 98 | 2020-12-01,5.72,entertainment 99 | 2020-12-01,73.53,groceries 100 | 2021-01-01,70.84,entertainment 101 | 2021-01-01,99.42,entertainment 102 | 2021-01-01,37.04,healthcare 103 | 2021-01-01,90.98,healthcare 104 | 2021-01-01,58.67,healthcare 105 | 2021-01-01,92.68,groceries 106 | 2021-01-01,65.82,groceries 107 | 2021-01-01,99.58,groceries 108 | 2021-01-01,16.65,healthcare 109 | 2021-01-01,10.93,groceries 110 | 2021-02-01,33.45,groceries 111 | 2021-02-01,70.84,travel 112 | 2021-02-01,34.73,entertainment 113 | 2021-02-01,46.42,groceries 114 | 2021-02-01,91.4,healthcare 115 | 2021-02-01,71.29,travel 116 | 2021-02-01,21.49,entertainment 117 | 2021-02-01,18.48,groceries 118 | 2021-02-01,55.53,healthcare 119 | 2021-02-01,77.15,groceries 120 | 2021-03-01,43.56,groceries 121 | 2021-03-01,83.79,healthcare 122 | 2021-03-01,84.38,travel 123 | 2021-03-01,24.06,groceries 124 | 2021-03-01,56.91,groceries 125 | 2021-03-01,9.95,travel 126 | 2021-03-01,17.04,housing 127 | 2021-03-01,65.6,entertainment 128 | 2021-03-01,48.31,housing 129 | 2021-03-01,53.55,healthcare 130 | 2021-04-01,85.99,car 131 | 2021-04-01,59.48,travel 132 | 2021-04-01,1.1,travel 133 | 2021-04-01,54.48,groceries 134 | 2021-04-01,14.25,car 135 | 2021-04-01,24.01,groceries 136 | 2021-04-01,66.32,healthcare 137 | 2021-04-01,38.34,healthcare 138 | 2021-04-01,62.12,groceries 139 | 2021-04-01,14.14,housing 140 | 2021-04-01,64.07,groceries 141 | 2021-04-01,97.5,housing 142 | 2021-04-01,97.26,entertainment 143 | 2021-04-01,90.99,entertainment 144 | 2021-05-01,64.94,car 145 | 2021-05-01,36.64,groceries 146 | 2021-05-01,70.0,groceries 147 | 2021-05-01,90.62,groceries 148 | 2021-05-01,92.65,groceries 149 | 2021-05-01,77.18,groceries 150 | 2021-05-01,57.91,groceries 151 | 2021-05-01,43.15,housing 152 | 2021-06-01,83.8,groceries 153 | 2021-06-01,49.62,entertainment 154 | 2021-06-01,71.06,groceries 155 | 2021-06-01,33.25,groceries 156 | 2021-06-01,53.4,groceries 157 | 2021-06-01,4.61,entertainment 158 | 2021-06-01,2.51,utilities 159 | 2021-06-01,56.95,entertainment 160 | 2021-06-01,82.12,travel 161 | 2021-06-01,29.88,entertainment 162 | 2021-06-01,1.36,utilities 163 | 2021-07-01,40.01,housing 164 | 2021-07-01,47.98,groceries 165 | 2021-07-01,45.75,groceries 166 | 2021-07-01,95.4,groceries 167 | 2021-07-01,90.35,groceries 168 | 2021-07-01,82.36,groceries 169 | 2021-07-01,92.92,car 170 | 2021-07-01,16.85,entertainment 171 | 2021-07-01,82.77,groceries 172 | 2021-07-01,63.85,utilities 173 | 2021-07-01,10.82,entertainment 174 | 2021-08-01,42.11,groceries 175 | 2021-08-01,10.76,groceries 176 | 2021-08-01,49.48,entertainment 177 | 2021-08-01,53.11,groceries 178 | 2021-08-01,51.99,groceries 179 | 2021-08-01,75.16,utilities 180 | 2021-08-01,53.6,groceries 181 | 2021-08-01,37.27,groceries 182 | 2021-08-01,11.23,groceries 183 | 2021-09-01,45.43,groceries 184 | 2021-09-01,73.96,groceries 185 | 2021-09-01,45.1,groceries 186 | 2021-09-01,25.35,groceries 187 | 2021-09-01,68.04,groceries 188 | 2021-09-01,62.09,groceries 189 | 2021-09-01,66.98,utilities 190 | 2021-09-01,32.28,groceries 191 | 2021-09-01,19.8,entertainment 192 | 2021-09-01,69.23,groceries 193 | 2021-10-01,56.28,healthcare 194 | 2021-10-01,2.16,entertainment 195 | 2021-10-01,59.04,groceries 196 | 2021-10-01,89.95,groceries 197 | 2021-10-01,35.4,groceries 198 | 2021-10-01,75.07,entertainment 199 | 2021-10-01,34.26,groceries 200 | 2021-11-01,95.83,groceries 201 | 2021-11-01,9.02,utilities 202 | 2021-11-01,85.69,healthcare 203 | 2021-11-01,24.64,healthcare 204 | 2021-11-01,49.18,healthcare 205 | 2021-11-01,53.08,entertainment 206 | 2021-11-01,29.73,travel 207 | 2021-11-01,17.31,groceries 208 | 2021-11-01,76.38,groceries 209 | 2021-11-01,89.54,groceries 210 | 2021-12-01,53.16,groceries 211 | 2021-12-01,53.25,entertainment 212 | 2021-12-01,91.38,car 213 | 2021-12-01,77.33,entertainment 214 | 2021-12-01,85.64,groceries 215 | 2021-12-01,61.59,entertainment 216 | 2021-12-01,43.79,car 217 | 2021-12-01,44.01,travel 218 | 2021-12-01,13.41,healthcare 219 | 2022-01-01,96.68,housing 220 | 2022-01-01,18.45,groceries 221 | 2022-01-01,78.89,groceries 222 | 2022-01-01,6.87,healthcare 223 | 2022-01-01,56.18,entertainment 224 | 2022-01-01,63.06,groceries 225 | 2022-01-01,15.97,groceries 226 | 2022-01-01,50.25,groceries 227 | 2022-01-01,58.76,groceries 228 | 2022-01-01,24.09,groceries 229 | 2022-01-01,31.04,groceries 230 | 2022-01-01,70.04,groceries 231 | 2022-01-01,25.06,groceries 232 | 2022-01-01,61.61,travel 233 | 2022-01-01,39.21,groceries 234 | 2022-01-01,11.46,groceries 235 | 2022-02-01,14.8,housing 236 | 2022-02-01,81.78,healthcare 237 | 2022-02-01,12.49,entertainment 238 | 2022-02-01,68.73,utilities 239 | 2022-02-01,30.66,groceries 240 | 2022-02-01,56.6,groceries 241 | 2022-02-01,73.0,entertainment 242 | 2022-02-01,7.76,travel 243 | 2022-03-01,22.03,groceries 244 | 2022-03-01,74.89,groceries 245 | 2022-03-01,27.95,groceries 246 | 2022-03-01,97.88,travel 247 | 2022-03-01,29.95,entertainment 248 | 2022-03-01,33.46,entertainment 249 | 2022-03-01,11.64,entertainment 250 | 2022-03-01,28.7,groceries 251 | 2022-03-01,84.11,groceries 252 | 2022-03-01,65.94,healthcare 253 | 2022-03-01,68.89,groceries 254 | 2022-03-01,99.26,car 255 | 2022-03-01,19.47,car 256 | 2022-03-01,79.93,entertainment 257 | 2022-04-01,81.24,groceries 258 | 2022-04-01,32.05,utilities 259 | 2022-04-01,74.43,utilities 260 | 2022-04-01,92.54,entertainment 261 | 2022-04-01,67.87,entertainment 262 | 2022-04-01,49.54,groceries 263 | 2022-04-01,72.31,entertainment 264 | 2022-04-01,74.85,groceries 265 | 2022-04-01,56.05,groceries 266 | 2022-04-01,69.44,travel 267 | 2022-04-01,85.56,groceries 268 | 2022-05-01,14.99,healthcare 269 | 2022-05-01,29.46,groceries 270 | 2022-05-01,17.41,groceries 271 | 2022-05-01,83.34,groceries 272 | 2022-05-01,40.07,groceries 273 | 2022-05-01,78.19,entertainment 274 | 2022-05-01,32.96,housing 275 | 2022-05-01,89.92,entertainment 276 | 2022-06-01,86.82,groceries 277 | 2022-06-01,69.78,entertainment 278 | 2022-06-01,66.43,groceries 279 | 2022-06-01,58.21,groceries 280 | 2022-06-01,73.24,groceries 281 | 2022-06-01,19.85,groceries 282 | 2022-06-01,47.97,housing 283 | -------------------------------------------------------------------------------- /part1/environment/README.md: -------------------------------------------------------------------------------- 1 | # Environment Setup 2 | 3 | ## Installation with Anaconda/Miniconda 4 | 5 | Run the two commands from the root directory. 6 | 7 | ```shell 8 | conda env create -f ./environment/conda.yaml 9 | conda activate dash-app-tutorial 10 | ``` 11 | 12 | ## Installation with Pip 13 | 14 | **Note:** Python >= 3.9 is required to use the [modern style](https://peps.python.org/pep-0585/) of type annotations. 15 | 16 | Run the command from the root directory. 17 | 18 | ```shell 19 | python -m pip install -r ./environment/requirements.txt 20 | ``` 21 | -------------------------------------------------------------------------------- /part1/environment/conda.yaml: -------------------------------------------------------------------------------- 1 | name: dash-app-tutorial 2 | dependencies: 3 | - python>=3.9 4 | - pip 5 | - pip: 6 | - -r requirements.txt 7 | -------------------------------------------------------------------------------- /part1/environment/requirements-dev.txt: -------------------------------------------------------------------------------- 1 | black 2 | pylint -------------------------------------------------------------------------------- /part1/environment/requirements.txt: -------------------------------------------------------------------------------- 1 | Babel 2 | dash 3 | dash-bootstrap-components 4 | pandas 5 | pandas-stubs 6 | plotly 7 | python-i18n[YAML] -------------------------------------------------------------------------------- /part1/main.py: -------------------------------------------------------------------------------- 1 | from dash import Dash 2 | from dash_bootstrap_components.themes import BOOTSTRAP 3 | 4 | from src.components.layout import create_layout 5 | 6 | 7 | def main() -> None: 8 | app = Dash(external_stylesheets=[BOOTSTRAP]) 9 | app.title = "Medal dashboard" 10 | app.layout = create_layout(app) 11 | app.run() 12 | 13 | 14 | if __name__ == "__main__": 15 | main() 16 | -------------------------------------------------------------------------------- /part1/src/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ArjanCodes/2022-dash/a0e56e00c98900485b0c75e5d5ce99b36ac5a2d2/part1/src/__init__.py -------------------------------------------------------------------------------- /part1/src/components/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ArjanCodes/2022-dash/a0e56e00c98900485b0c75e5d5ce99b36ac5a2d2/part1/src/components/__init__.py -------------------------------------------------------------------------------- /part1/src/components/bar_chart.py: -------------------------------------------------------------------------------- 1 | import plotly.express as px 2 | from dash import Dash, dcc, html 3 | from dash.dependencies import Input, Output 4 | 5 | from . import ids 6 | 7 | MEDAL_DATA = px.data.medals_long() 8 | 9 | 10 | def render(app: Dash) -> html.Div: 11 | @app.callback( 12 | Output(ids.BAR_CHART, "children"), 13 | [ 14 | Input(ids.NATION_DROPDOWN, "value"), 15 | ], 16 | ) 17 | def update_bar_chart(nations: list[str]) -> html.Div: 18 | filtered_data = MEDAL_DATA.query("nation in @nations") 19 | 20 | if filtered_data.shape[0] == 0: 21 | return html.Div("No data selected.", id=ids.BAR_CHART) 22 | 23 | fig = px.bar(filtered_data, x="medal", y="count", color="nation", text="nation") 24 | 25 | return html.Div(dcc.Graph(figure=fig), id=ids.BAR_CHART) 26 | 27 | return html.Div(id=ids.BAR_CHART) 28 | -------------------------------------------------------------------------------- /part1/src/components/ids.py: -------------------------------------------------------------------------------- 1 | BAR_CHART = "bar-chart" 2 | 3 | NATION_DROPDOWN = "nation-dropdown" 4 | SELECT_ALL_NATIONS_BUTTON = "select-all-nations-button" 5 | -------------------------------------------------------------------------------- /part1/src/components/layout.py: -------------------------------------------------------------------------------- 1 | from dash import Dash, html 2 | 3 | from . import bar_chart, nation_dropdown 4 | 5 | 6 | def create_layout(app: Dash) -> html.Div: 7 | return html.Div( 8 | className="app-div", 9 | children=[ 10 | html.H1(app.title), 11 | html.Hr(), 12 | html.Div( 13 | className="dropdown-container", 14 | children=[ 15 | nation_dropdown.render(app), 16 | ], 17 | ), 18 | bar_chart.render(app), 19 | ], 20 | ) 21 | -------------------------------------------------------------------------------- /part1/src/components/nation_dropdown.py: -------------------------------------------------------------------------------- 1 | from dash import Dash, dcc, html 2 | from dash.dependencies import Input, Output 3 | 4 | from . import ids 5 | 6 | 7 | def render(app: Dash) -> html.Div: 8 | all_nations = ["South Korea", "China", "Canada"] 9 | 10 | @app.callback( 11 | Output(ids.NATION_DROPDOWN, "value"), 12 | Input(ids.SELECT_ALL_NATIONS_BUTTON, "n_clicks"), 13 | ) 14 | def select_all_nations(_: int) -> list[str]: 15 | return all_nations 16 | 17 | return html.Div( 18 | children=[ 19 | html.H6("Nation"), 20 | dcc.Dropdown( 21 | id=ids.NATION_DROPDOWN, 22 | options=[{"label": year, "value": year} for year in all_nations], 23 | value=all_nations, 24 | multi=True, 25 | ), 26 | html.Button( 27 | className="dropdown-button", 28 | children=["Select All"], 29 | id=ids.SELECT_ALL_NATIONS_BUTTON, 30 | n_clicks=0, 31 | ), 32 | ] 33 | ) 34 | -------------------------------------------------------------------------------- /part2/README.md: -------------------------------------------------------------------------------- 1 | # Dash App Tutorial 2 | 3 | ## Running the app 4 | 5 | Run the following commands and open the local host web address chosen by Dash. 6 | 7 | ```shell 8 | python ./main.py 9 | ``` 10 | 11 | An example of the expected terminal messages are shown below: 12 | 13 | ```shell 14 | Dash is running on http://127.0.0.1:8050/ 15 | 16 | * Serving Flask app 'app' (lazy loading) 17 | * Environment: production 18 | WARNING: This is a development server. Do not use it in a production deployment. 19 | Use a production WSGI server instead. 20 | * Debug mode: on 21 | ``` 22 | -------------------------------------------------------------------------------- /part2/assets/style.css: -------------------------------------------------------------------------------- 1 | hr { 2 | border-top: 2px solid gray; 3 | } 4 | 5 | .dropdown-container { 6 | display: grid; 7 | grid-template-columns: repeat(3, 1fr); 8 | gap: 10px; 9 | } 10 | 11 | .dropdown-button { 12 | width: 100%; 13 | margin-top: 5px; 14 | } 15 | 16 | .tabs-group { 17 | padding-top: 20px; 18 | } 19 | 20 | .app-div { 21 | padding: 10px; 22 | } 23 | 24 | .data-table-div { 25 | margin: auto; 26 | padding: 10px; 27 | } 28 | 29 | .budget-label { 30 | font-size: 20px; 31 | font-weight: bold; 32 | } 33 | 34 | .budget-text { 35 | font-size: 20px; 36 | } 37 | 38 | .budget-text-value { 39 | font-size: 20px; 40 | font-style: italic; 41 | } 42 | 43 | .budget-progress-bar { 44 | height: 20px; 45 | } 46 | 47 | .budget-tooltip { 48 | font-size: 18px; 49 | } 50 | 51 | .checklist { 52 | text-align: center; 53 | padding-top: 10px; 54 | padding-bottom: 20px; 55 | } 56 | 57 | @media(max-width: 800px) { 58 | .dropdown-container { 59 | grid-template-columns: 1fr; 60 | } 61 | } -------------------------------------------------------------------------------- /part2/data/transactions.csv: -------------------------------------------------------------------------------- 1 | date,amount,category 2 | 2020-01-01,89.92,groceries 3 | 2020-01-01,48.74,entertainment 4 | 2020-01-01,39.08,groceries 5 | 2020-01-01,48.36,groceries 6 | 2020-01-01,77.36,groceries 7 | 2020-01-01,73.46,groceries 8 | 2020-01-01,57.16,groceries 9 | 2020-01-01,75.94,groceries 10 | 2020-02-01,76.29,healthcare 11 | 2020-02-01,16.09,entertainment 12 | 2020-02-01,82.62,groceries 13 | 2020-02-01,21.61,utilities 14 | 2020-02-01,43.55,entertainment 15 | 2020-02-01,28.57,car 16 | 2020-02-01,86.28,groceries 17 | 2020-02-01,43.51,housing 18 | 2020-02-01,7.93,car 19 | 2020-03-01,51.83,groceries 20 | 2020-03-01,48.8,travel 21 | 2020-03-01,30.33,groceries 22 | 2020-03-01,85.14,groceries 23 | 2020-03-01,96.06,groceries 24 | 2020-03-01,66.16,housing 25 | 2020-03-01,53.73,groceries 26 | 2020-03-01,64.25,healthcare 27 | 2020-03-01,94.17,groceries 28 | 2020-04-01,65.61,housing 29 | 2020-04-01,11.49,travel 30 | 2020-04-01,88.06,groceries 31 | 2020-04-01,58.63,groceries 32 | 2020-04-01,28.14,groceries 33 | 2020-04-01,32.49,utilities 34 | 2020-04-01,6.55,groceries 35 | 2020-04-01,94.11,groceries 36 | 2020-05-01,1.19,groceries 37 | 2020-05-01,56.66,groceries 38 | 2020-05-01,49.05,entertainment 39 | 2020-05-01,41.46,healthcare 40 | 2020-05-01,71.48,entertainment 41 | 2020-05-01,32.84,groceries 42 | 2020-05-01,78.47,entertainment 43 | 2020-05-01,29.33,groceries 44 | 2020-06-01,87.67,groceries 45 | 2020-06-01,44.3,housing 46 | 2020-06-01,56.0,entertainment 47 | 2020-06-01,66.25,travel 48 | 2020-06-01,97.5,healthcare 49 | 2020-06-01,29.02,groceries 50 | 2020-06-01,97.86,groceries 51 | 2020-07-01,47.77,housing 52 | 2020-07-01,80.89,groceries 53 | 2020-07-01,21.09,utilities 54 | 2020-07-01,4.62,car 55 | 2020-07-01,96.13,utilities 56 | 2020-07-01,12.53,groceries 57 | 2020-07-01,35.77,entertainment 58 | 2020-08-01,82.73,healthcare 59 | 2020-08-01,47.77,groceries 60 | 2020-08-01,74.13,groceries 61 | 2020-08-01,38.33,entertainment 62 | 2020-08-01,65.55,housing 63 | 2020-08-01,6.29,groceries 64 | 2020-08-01,25.23,groceries 65 | 2020-09-01,49.51,travel 66 | 2020-09-01,17.92,entertainment 67 | 2020-09-01,74.69,travel 68 | 2020-09-01,89.4,groceries 69 | 2020-09-01,55.0,travel 70 | 2020-09-01,81.93,utilities 71 | 2020-09-01,57.69,healthcare 72 | 2020-09-01,22.22,entertainment 73 | 2020-09-01,13.22,entertainment 74 | 2020-09-01,77.76,groceries 75 | 2020-09-01,13.22,groceries 76 | 2020-10-01,57.58,groceries 77 | 2020-10-01,4.0,car 78 | 2020-10-01,20.48,utilities 79 | 2020-10-01,15.74,utilities 80 | 2020-10-01,59.31,entertainment 81 | 2020-10-01,15.31,groceries 82 | 2020-10-01,96.55,groceries 83 | 2020-10-01,91.65,entertainment 84 | 2020-10-01,50.04,groceries 85 | 2020-10-01,80.78,groceries 86 | 2020-11-01,71.81,groceries 87 | 2020-11-01,24.68,housing 88 | 2020-11-01,69.69,healthcare 89 | 2020-11-01,56.34,groceries 90 | 2020-12-01,66.58,entertainment 91 | 2020-12-01,76.24,utilities 92 | 2020-12-01,81.17,groceries 93 | 2020-12-01,47.01,car 94 | 2020-12-01,71.57,groceries 95 | 2020-12-01,73.81,utilities 96 | 2020-12-01,9.22,healthcare 97 | 2020-12-01,32.22,travel 98 | 2020-12-01,5.72,entertainment 99 | 2020-12-01,73.53,groceries 100 | 2021-01-01,70.84,entertainment 101 | 2021-01-01,99.42,entertainment 102 | 2021-01-01,37.04,healthcare 103 | 2021-01-01,90.98,healthcare 104 | 2021-01-01,58.67,healthcare 105 | 2021-01-01,92.68,groceries 106 | 2021-01-01,65.82,groceries 107 | 2021-01-01,99.58,groceries 108 | 2021-01-01,16.65,healthcare 109 | 2021-01-01,10.93,groceries 110 | 2021-02-01,33.45,groceries 111 | 2021-02-01,70.84,travel 112 | 2021-02-01,34.73,entertainment 113 | 2021-02-01,46.42,groceries 114 | 2021-02-01,91.4,healthcare 115 | 2021-02-01,71.29,travel 116 | 2021-02-01,21.49,entertainment 117 | 2021-02-01,18.48,groceries 118 | 2021-02-01,55.53,healthcare 119 | 2021-02-01,77.15,groceries 120 | 2021-03-01,43.56,groceries 121 | 2021-03-01,83.79,healthcare 122 | 2021-03-01,84.38,travel 123 | 2021-03-01,24.06,groceries 124 | 2021-03-01,56.91,groceries 125 | 2021-03-01,9.95,travel 126 | 2021-03-01,17.04,housing 127 | 2021-03-01,65.6,entertainment 128 | 2021-03-01,48.31,housing 129 | 2021-03-01,53.55,healthcare 130 | 2021-04-01,85.99,car 131 | 2021-04-01,59.48,travel 132 | 2021-04-01,1.1,travel 133 | 2021-04-01,54.48,groceries 134 | 2021-04-01,14.25,car 135 | 2021-04-01,24.01,groceries 136 | 2021-04-01,66.32,healthcare 137 | 2021-04-01,38.34,healthcare 138 | 2021-04-01,62.12,groceries 139 | 2021-04-01,14.14,housing 140 | 2021-04-01,64.07,groceries 141 | 2021-04-01,97.5,housing 142 | 2021-04-01,97.26,entertainment 143 | 2021-04-01,90.99,entertainment 144 | 2021-05-01,64.94,car 145 | 2021-05-01,36.64,groceries 146 | 2021-05-01,70.0,groceries 147 | 2021-05-01,90.62,groceries 148 | 2021-05-01,92.65,groceries 149 | 2021-05-01,77.18,groceries 150 | 2021-05-01,57.91,groceries 151 | 2021-05-01,43.15,housing 152 | 2021-06-01,83.8,groceries 153 | 2021-06-01,49.62,entertainment 154 | 2021-06-01,71.06,groceries 155 | 2021-06-01,33.25,groceries 156 | 2021-06-01,53.4,groceries 157 | 2021-06-01,4.61,entertainment 158 | 2021-06-01,2.51,utilities 159 | 2021-06-01,56.95,entertainment 160 | 2021-06-01,82.12,travel 161 | 2021-06-01,29.88,entertainment 162 | 2021-06-01,1.36,utilities 163 | 2021-07-01,40.01,housing 164 | 2021-07-01,47.98,groceries 165 | 2021-07-01,45.75,groceries 166 | 2021-07-01,95.4,groceries 167 | 2021-07-01,90.35,groceries 168 | 2021-07-01,82.36,groceries 169 | 2021-07-01,92.92,car 170 | 2021-07-01,16.85,entertainment 171 | 2021-07-01,82.77,groceries 172 | 2021-07-01,63.85,utilities 173 | 2021-07-01,10.82,entertainment 174 | 2021-08-01,42.11,groceries 175 | 2021-08-01,10.76,groceries 176 | 2021-08-01,49.48,entertainment 177 | 2021-08-01,53.11,groceries 178 | 2021-08-01,51.99,groceries 179 | 2021-08-01,75.16,utilities 180 | 2021-08-01,53.6,groceries 181 | 2021-08-01,37.27,groceries 182 | 2021-08-01,11.23,groceries 183 | 2021-09-01,45.43,groceries 184 | 2021-09-01,73.96,groceries 185 | 2021-09-01,45.1,groceries 186 | 2021-09-01,25.35,groceries 187 | 2021-09-01,68.04,groceries 188 | 2021-09-01,62.09,groceries 189 | 2021-09-01,66.98,utilities 190 | 2021-09-01,32.28,groceries 191 | 2021-09-01,19.8,entertainment 192 | 2021-09-01,69.23,groceries 193 | 2021-10-01,56.28,healthcare 194 | 2021-10-01,2.16,entertainment 195 | 2021-10-01,59.04,groceries 196 | 2021-10-01,89.95,groceries 197 | 2021-10-01,35.4,groceries 198 | 2021-10-01,75.07,entertainment 199 | 2021-10-01,34.26,groceries 200 | 2021-11-01,95.83,groceries 201 | 2021-11-01,9.02,utilities 202 | 2021-11-01,85.69,healthcare 203 | 2021-11-01,24.64,healthcare 204 | 2021-11-01,49.18,healthcare 205 | 2021-11-01,53.08,entertainment 206 | 2021-11-01,29.73,travel 207 | 2021-11-01,17.31,groceries 208 | 2021-11-01,76.38,groceries 209 | 2021-11-01,89.54,groceries 210 | 2021-12-01,53.16,groceries 211 | 2021-12-01,53.25,entertainment 212 | 2021-12-01,91.38,car 213 | 2021-12-01,77.33,entertainment 214 | 2021-12-01,85.64,groceries 215 | 2021-12-01,61.59,entertainment 216 | 2021-12-01,43.79,car 217 | 2021-12-01,44.01,travel 218 | 2021-12-01,13.41,healthcare 219 | 2022-01-01,96.68,housing 220 | 2022-01-01,18.45,groceries 221 | 2022-01-01,78.89,groceries 222 | 2022-01-01,6.87,healthcare 223 | 2022-01-01,56.18,entertainment 224 | 2022-01-01,63.06,groceries 225 | 2022-01-01,15.97,groceries 226 | 2022-01-01,50.25,groceries 227 | 2022-01-01,58.76,groceries 228 | 2022-01-01,24.09,groceries 229 | 2022-01-01,31.04,groceries 230 | 2022-01-01,70.04,groceries 231 | 2022-01-01,25.06,groceries 232 | 2022-01-01,61.61,travel 233 | 2022-01-01,39.21,groceries 234 | 2022-01-01,11.46,groceries 235 | 2022-02-01,14.8,housing 236 | 2022-02-01,81.78,healthcare 237 | 2022-02-01,12.49,entertainment 238 | 2022-02-01,68.73,utilities 239 | 2022-02-01,30.66,groceries 240 | 2022-02-01,56.6,groceries 241 | 2022-02-01,73.0,entertainment 242 | 2022-02-01,7.76,travel 243 | 2022-03-01,22.03,groceries 244 | 2022-03-01,74.89,groceries 245 | 2022-03-01,27.95,groceries 246 | 2022-03-01,97.88,travel 247 | 2022-03-01,29.95,entertainment 248 | 2022-03-01,33.46,entertainment 249 | 2022-03-01,11.64,entertainment 250 | 2022-03-01,28.7,groceries 251 | 2022-03-01,84.11,groceries 252 | 2022-03-01,65.94,healthcare 253 | 2022-03-01,68.89,groceries 254 | 2022-03-01,99.26,car 255 | 2022-03-01,19.47,car 256 | 2022-03-01,79.93,entertainment 257 | 2022-04-01,81.24,groceries 258 | 2022-04-01,32.05,utilities 259 | 2022-04-01,74.43,utilities 260 | 2022-04-01,92.54,entertainment 261 | 2022-04-01,67.87,entertainment 262 | 2022-04-01,49.54,groceries 263 | 2022-04-01,72.31,entertainment 264 | 2022-04-01,74.85,groceries 265 | 2022-04-01,56.05,groceries 266 | 2022-04-01,69.44,travel 267 | 2022-04-01,85.56,groceries 268 | 2022-05-01,14.99,healthcare 269 | 2022-05-01,29.46,groceries 270 | 2022-05-01,17.41,groceries 271 | 2022-05-01,83.34,groceries 272 | 2022-05-01,40.07,groceries 273 | 2022-05-01,78.19,entertainment 274 | 2022-05-01,32.96,housing 275 | 2022-05-01,89.92,entertainment 276 | 2022-06-01,86.82,groceries 277 | 2022-06-01,69.78,entertainment 278 | 2022-06-01,66.43,groceries 279 | 2022-06-01,58.21,groceries 280 | 2022-06-01,73.24,groceries 281 | 2022-06-01,19.85,groceries 282 | 2022-06-01,47.97,housing 283 | -------------------------------------------------------------------------------- /part2/environment/README.md: -------------------------------------------------------------------------------- 1 | # Environment Setup 2 | 3 | ## Installation with Anaconda/Miniconda 4 | 5 | Run the two commands from the root directory. 6 | 7 | ```shell 8 | conda env create -f ./environment/conda.yaml 9 | conda activate dash-app-tutorial 10 | ``` 11 | 12 | ## Installation with Pip 13 | 14 | **Note:** Python >= 3.9 is required to use the [modern style](https://peps.python.org/pep-0585/) of type annotations. 15 | 16 | Run the command from the root directory. 17 | 18 | ```shell 19 | python -m pip install -r ./environment/requirements.txt 20 | ``` 21 | -------------------------------------------------------------------------------- /part2/environment/conda.yaml: -------------------------------------------------------------------------------- 1 | name: dash-app-tutorial 2 | dependencies: 3 | - python>=3.9 4 | - pip 5 | - pip: 6 | - -r requirements.txt 7 | -------------------------------------------------------------------------------- /part2/environment/requirements-dev.txt: -------------------------------------------------------------------------------- 1 | black 2 | pylint -------------------------------------------------------------------------------- /part2/environment/requirements.txt: -------------------------------------------------------------------------------- 1 | Babel 2 | dash 3 | dash-bootstrap-components 4 | pandas 5 | pandas-stubs 6 | plotly 7 | python-i18n[YAML] -------------------------------------------------------------------------------- /part2/main.py: -------------------------------------------------------------------------------- 1 | from dash import Dash 2 | from dash_bootstrap_components.themes import BOOTSTRAP 3 | 4 | from src.components.layout import create_layout 5 | from src.data.loader import load_transaction_data 6 | 7 | DATA_PATH = "./data/transactions.csv" 8 | 9 | 10 | def main() -> None: 11 | 12 | # load the data and create the data manager 13 | data = load_transaction_data(DATA_PATH) 14 | 15 | app = Dash(external_stylesheets=[BOOTSTRAP]) 16 | app.title = "Financial Dashboard" 17 | app.layout = create_layout(app, data) 18 | app.run() 19 | 20 | 21 | if __name__ == "__main__": 22 | main() 23 | -------------------------------------------------------------------------------- /part2/src/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ArjanCodes/2022-dash/a0e56e00c98900485b0c75e5d5ce99b36ac5a2d2/part2/src/__init__.py -------------------------------------------------------------------------------- /part2/src/components/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ArjanCodes/2022-dash/a0e56e00c98900485b0c75e5d5ce99b36ac5a2d2/part2/src/components/__init__.py -------------------------------------------------------------------------------- /part2/src/components/bar_chart.py: -------------------------------------------------------------------------------- 1 | import pandas as pd 2 | import plotly.express as px 3 | from dash import Dash, dcc, html 4 | from dash.dependencies import Input, Output 5 | 6 | from ..data.loader import DataSchema 7 | from . import ids 8 | 9 | 10 | def render(app: Dash, data: pd.DataFrame) -> html.Div: 11 | @app.callback( 12 | Output(ids.BAR_CHART, "children"), 13 | [ 14 | Input(ids.YEAR_DROPDOWN, "value"), 15 | Input(ids.MONTH_DROPDOWN, "value"), 16 | Input(ids.CATEGORY_DROPDOWN, "value"), 17 | ], 18 | ) 19 | def update_bar_chart( 20 | years: list[str], months: list[str], categories: list[str] 21 | ) -> html.Div: 22 | filtered_data = data.query( 23 | "year in @years and month in @months and category in @categories" 24 | ) 25 | 26 | if filtered_data.shape[0] == 0: 27 | return html.Div("No data selected.", id=ids.BAR_CHART) 28 | 29 | def create_pivot_table() -> pd.DataFrame: 30 | pt = filtered_data.pivot_table( 31 | values=DataSchema.AMOUNT, 32 | index=[DataSchema.CATEGORY], 33 | aggfunc="sum", 34 | fill_value=0, 35 | dropna=False, 36 | ) 37 | return pt.reset_index().sort_values(DataSchema.AMOUNT, ascending=False) 38 | 39 | fig = px.bar( 40 | create_pivot_table(), 41 | x=DataSchema.CATEGORY, 42 | y=DataSchema.AMOUNT, 43 | color=DataSchema.CATEGORY, 44 | ) 45 | 46 | return html.Div(dcc.Graph(figure=fig), id=ids.BAR_CHART) 47 | 48 | return html.Div(id=ids.BAR_CHART) 49 | -------------------------------------------------------------------------------- /part2/src/components/category_dropdown.py: -------------------------------------------------------------------------------- 1 | import pandas as pd 2 | from dash import Dash, dcc, html 3 | from dash.dependencies import Input, Output 4 | 5 | from ..data.loader import DataSchema 6 | from . import ids 7 | 8 | 9 | def render(app: Dash, data: pd.DataFrame) -> html.Div: 10 | all_categories: list[str] = data[DataSchema.CATEGORY].tolist() 11 | unique_categories: list[str] = sorted(set(all_categories)) 12 | 13 | @app.callback( 14 | Output(ids.CATEGORY_DROPDOWN, "value"), 15 | [ 16 | Input(ids.YEAR_DROPDOWN, "value"), 17 | Input(ids.MONTH_DROPDOWN, "value"), 18 | Input(ids.SELECT_ALL_CATEGORIES_BUTTON, "n_clicks"), 19 | ], 20 | ) 21 | def select_all_categories(years: list[str], months: list[str], _: int) -> list[str]: 22 | filtered_data = data.query("year in @years and month in @months") 23 | return sorted(set(filtered_data[DataSchema.CATEGORY].tolist())) 24 | 25 | return html.Div( 26 | children=[ 27 | html.H6("Category"), 28 | dcc.Dropdown( 29 | id=ids.CATEGORY_DROPDOWN, 30 | options=[ 31 | {"label": category, "value": category} 32 | for category in unique_categories 33 | ], 34 | value=unique_categories, 35 | multi=True, 36 | placeholder="Select", 37 | ), 38 | html.Button( 39 | className="dropdown-button", 40 | children=["Select All"], 41 | id=ids.SELECT_ALL_CATEGORIES_BUTTON, 42 | n_clicks=0, 43 | ), 44 | ], 45 | ) 46 | -------------------------------------------------------------------------------- /part2/src/components/ids.py: -------------------------------------------------------------------------------- 1 | BAR_CHART = "bar-chart" 2 | PIE_CHART = "pie-chart" 3 | 4 | SELECT_ALL_CATEGORIES_BUTTON = "select-all-categories-button" 5 | CATEGORY_DROPDOWN = "category-dropdown" 6 | 7 | SELECT_ALL_MONTHS_BUTTON = "select-all-months-button" 8 | MONTH_DROPDOWN = "month-dropdown" 9 | 10 | YEAR_DROPDOWN = "year-dropdown" 11 | SELECT_ALL_YEARS_BUTTON = "select-all-years-button" 12 | -------------------------------------------------------------------------------- /part2/src/components/layout.py: -------------------------------------------------------------------------------- 1 | import pandas as pd 2 | from dash import Dash, html 3 | from src.components import ( 4 | bar_chart, 5 | category_dropdown, 6 | month_dropdown, 7 | pie_chart, 8 | year_dropdown, 9 | ) 10 | 11 | 12 | def create_layout(app: Dash, data: pd.DataFrame) -> html.Div: 13 | return html.Div( 14 | className="app-div", 15 | children=[ 16 | html.H1(app.title), 17 | html.Hr(), 18 | html.Div( 19 | className="dropdown-container", 20 | children=[ 21 | year_dropdown.render(app, data), 22 | month_dropdown.render(app, data), 23 | category_dropdown.render(app, data), 24 | ], 25 | ), 26 | bar_chart.render(app, data), 27 | pie_chart.render(app, data), 28 | ], 29 | ) 30 | -------------------------------------------------------------------------------- /part2/src/components/month_dropdown.py: -------------------------------------------------------------------------------- 1 | import pandas as pd 2 | from dash import Dash, dcc, html 3 | from dash.dependencies import Input, Output 4 | 5 | from ..data.loader import DataSchema 6 | from . import ids 7 | 8 | 9 | def render(app: Dash, data: pd.DataFrame) -> html.Div: 10 | all_months: list[str] = data[DataSchema.MONTH].tolist() 11 | unique_months = sorted(set(all_months)) 12 | 13 | @app.callback( 14 | Output(ids.MONTH_DROPDOWN, "value"), 15 | [ 16 | Input(ids.YEAR_DROPDOWN, "value"), 17 | Input(ids.SELECT_ALL_MONTHS_BUTTON, "n_clicks"), 18 | ], 19 | ) 20 | def select_all_months(years: list[str], _: int) -> list[str]: 21 | filtered_data = data.query("year in @years") 22 | return sorted(set(filtered_data[DataSchema.MONTH].tolist())) 23 | 24 | return html.Div( 25 | children=[ 26 | html.H6("Month"), 27 | dcc.Dropdown( 28 | id=ids.MONTH_DROPDOWN, 29 | options=[{"label": month, "value": month} for month in unique_months], 30 | value=unique_months, 31 | multi=True, 32 | ), 33 | html.Button( 34 | className="dropdown-button", 35 | children=["Select All"], 36 | id=ids.SELECT_ALL_MONTHS_BUTTON, 37 | n_clicks=0, 38 | ), 39 | ] 40 | ) 41 | -------------------------------------------------------------------------------- /part2/src/components/pie_chart.py: -------------------------------------------------------------------------------- 1 | import pandas as pd 2 | import plotly.graph_objects as go 3 | from dash import Dash, dcc, html 4 | from dash.dependencies import Input, Output 5 | 6 | from ..data.loader import DataSchema 7 | from . import ids 8 | 9 | 10 | def render(app: Dash, data: pd.DataFrame) -> html.Div: 11 | @app.callback( 12 | Output(ids.PIE_CHART, "children"), 13 | [ 14 | Input(ids.YEAR_DROPDOWN, "value"), 15 | Input(ids.MONTH_DROPDOWN, "value"), 16 | Input(ids.CATEGORY_DROPDOWN, "value"), 17 | ], 18 | ) 19 | def update_pie_chart( 20 | years: list[str], months: list[str], categories: list[str] 21 | ) -> html.Div: 22 | filtered_data = data.query( 23 | "year in @years and month in @months and category in @categories" 24 | ) 25 | 26 | if filtered_data.shape[0] == 0: 27 | return html.Div("No data selected.", id=ids.PIE_CHART) 28 | 29 | pie = go.Pie( 30 | labels=filtered_data[DataSchema.CATEGORY].tolist(), 31 | values=filtered_data[DataSchema.AMOUNT].tolist(), 32 | hole=0.5, 33 | ) 34 | 35 | fig = go.Figure(data=[pie]) 36 | fig.update_layout(margin={"t": 40, "b": 0, "l": 0, "r": 0}) 37 | fig.update_traces(hovertemplate="%{label}
$%{value:.2f}") 38 | 39 | return html.Div(dcc.Graph(figure=fig), id=ids.PIE_CHART) 40 | 41 | return html.Div(id=ids.PIE_CHART) 42 | -------------------------------------------------------------------------------- /part2/src/components/year_dropdown.py: -------------------------------------------------------------------------------- 1 | import pandas as pd 2 | from dash import Dash, dcc, html 3 | from dash.dependencies import Input, Output 4 | 5 | from ..data.loader import DataSchema 6 | from . import ids 7 | 8 | 9 | def render(app: Dash, data: pd.DataFrame) -> html.Div: 10 | all_years: list[str] = data[DataSchema.YEAR].tolist() 11 | unique_years = sorted(set(all_years), key=int) 12 | 13 | @app.callback( 14 | Output(ids.YEAR_DROPDOWN, "value"), 15 | Input(ids.SELECT_ALL_YEARS_BUTTON, "n_clicks"), 16 | ) 17 | def select_all_years(_: int) -> list[str]: 18 | return unique_years 19 | 20 | return html.Div( 21 | children=[ 22 | html.H6("Year"), 23 | dcc.Dropdown( 24 | id=ids.YEAR_DROPDOWN, 25 | options=[{"label": year, "value": year} for year in unique_years], 26 | value=unique_years, 27 | multi=True, 28 | ), 29 | html.Button( 30 | className="dropdown-button", 31 | children=["Select All"], 32 | id=ids.SELECT_ALL_YEARS_BUTTON, 33 | n_clicks=0, 34 | ), 35 | ] 36 | ) 37 | -------------------------------------------------------------------------------- /part2/src/data/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ArjanCodes/2022-dash/a0e56e00c98900485b0c75e5d5ce99b36ac5a2d2/part2/src/data/__init__.py -------------------------------------------------------------------------------- /part2/src/data/loader.py: -------------------------------------------------------------------------------- 1 | import pandas as pd 2 | 3 | 4 | class DataSchema: 5 | AMOUNT = "amount" 6 | CATEGORY = "category" 7 | DATE = "date" 8 | MONTH = "month" 9 | YEAR = "year" 10 | 11 | 12 | def load_transaction_data(path: str) -> pd.DataFrame: 13 | # load the data from the CSV file 14 | data = pd.read_csv( 15 | path, 16 | dtype={ 17 | DataSchema.AMOUNT: float, 18 | DataSchema.CATEGORY: str, 19 | DataSchema.DATE: str, 20 | }, 21 | parse_dates=[DataSchema.DATE], 22 | ) 23 | data[DataSchema.YEAR] = data[DataSchema.DATE].dt.year.astype(str) 24 | data[DataSchema.MONTH] = data[DataSchema.DATE].dt.month.astype(str) 25 | return data 26 | -------------------------------------------------------------------------------- /part3/README.md: -------------------------------------------------------------------------------- 1 | # Dash App Tutorial 2 | 3 | ## Running the app 4 | 5 | Run the following commands and open the local host web address chosen by Dash. 6 | 7 | ```shell 8 | python ./main.py 9 | ``` 10 | 11 | An example of the expected terminal messages are shown below: 12 | 13 | ```shell 14 | Dash is running on http://127.0.0.1:8050/ 15 | 16 | * Serving Flask app 'app' (lazy loading) 17 | * Environment: production 18 | WARNING: This is a development server. Do not use it in a production deployment. 19 | Use a production WSGI server instead. 20 | * Debug mode: on 21 | ``` 22 | -------------------------------------------------------------------------------- /part3/assets/style.css: -------------------------------------------------------------------------------- 1 | hr { 2 | border-top: 2px solid gray; 3 | } 4 | 5 | .dropdown-container { 6 | display: grid; 7 | grid-template-columns: repeat(3, 1fr); 8 | gap: 10px; 9 | } 10 | 11 | .dropdown-button { 12 | width: 100%; 13 | margin-top: 5px; 14 | } 15 | 16 | .tabs-group { 17 | padding-top: 20px; 18 | } 19 | 20 | .app-div { 21 | padding: 10px; 22 | } 23 | 24 | .data-table-div { 25 | margin: auto; 26 | padding: 10px; 27 | } 28 | 29 | .budget-label { 30 | font-size: 20px; 31 | font-weight: bold; 32 | } 33 | 34 | .budget-text { 35 | font-size: 20px; 36 | } 37 | 38 | .budget-text-value { 39 | font-size: 20px; 40 | font-style: italic; 41 | } 42 | 43 | .budget-progress-bar { 44 | height: 20px; 45 | } 46 | 47 | .budget-tooltip { 48 | font-size: 18px; 49 | } 50 | 51 | .checklist { 52 | text-align: center; 53 | padding-top: 10px; 54 | padding-bottom: 20px; 55 | } 56 | 57 | @media(max-width: 800px) { 58 | .dropdown-container { 59 | grid-template-columns: 1fr; 60 | } 61 | } -------------------------------------------------------------------------------- /part3/data/transactions.csv: -------------------------------------------------------------------------------- 1 | date,amount,category 2 | 2020-01-01,89.92,groceries 3 | 2020-01-01,48.74,entertainment 4 | 2020-01-01,39.08,groceries 5 | 2020-01-01,48.36,groceries 6 | 2020-01-01,77.36,groceries 7 | 2020-01-01,73.46,groceries 8 | 2020-01-01,57.16,groceries 9 | 2020-01-01,75.94,groceries 10 | 2020-02-01,76.29,healthcare 11 | 2020-02-01,16.09,entertainment 12 | 2020-02-01,82.62,groceries 13 | 2020-02-01,21.61,utilities 14 | 2020-02-01,43.55,entertainment 15 | 2020-02-01,28.57,car 16 | 2020-02-01,86.28,groceries 17 | 2020-02-01,43.51,housing 18 | 2020-02-01,7.93,car 19 | 2020-03-01,51.83,groceries 20 | 2020-03-01,48.8,travel 21 | 2020-03-01,30.33,groceries 22 | 2020-03-01,85.14,groceries 23 | 2020-03-01,96.06,groceries 24 | 2020-03-01,66.16,housing 25 | 2020-03-01,53.73,groceries 26 | 2020-03-01,64.25,healthcare 27 | 2020-03-01,94.17,groceries 28 | 2020-04-01,65.61,housing 29 | 2020-04-01,11.49,travel 30 | 2020-04-01,88.06,groceries 31 | 2020-04-01,58.63,groceries 32 | 2020-04-01,28.14,groceries 33 | 2020-04-01,32.49,utilities 34 | 2020-04-01,6.55,groceries 35 | 2020-04-01,94.11,groceries 36 | 2020-05-01,1.19,groceries 37 | 2020-05-01,56.66,groceries 38 | 2020-05-01,49.05,entertainment 39 | 2020-05-01,41.46,healthcare 40 | 2020-05-01,71.48,entertainment 41 | 2020-05-01,32.84,groceries 42 | 2020-05-01,78.47,entertainment 43 | 2020-05-01,29.33,groceries 44 | 2020-06-01,87.67,groceries 45 | 2020-06-01,44.3,housing 46 | 2020-06-01,56.0,entertainment 47 | 2020-06-01,66.25,travel 48 | 2020-06-01,97.5,healthcare 49 | 2020-06-01,29.02,groceries 50 | 2020-06-01,97.86,groceries 51 | 2020-07-01,47.77,housing 52 | 2020-07-01,80.89,groceries 53 | 2020-07-01,21.09,utilities 54 | 2020-07-01,4.62,car 55 | 2020-07-01,96.13,utilities 56 | 2020-07-01,12.53,groceries 57 | 2020-07-01,35.77,entertainment 58 | 2020-08-01,82.73,healthcare 59 | 2020-08-01,47.77,groceries 60 | 2020-08-01,74.13,groceries 61 | 2020-08-01,38.33,entertainment 62 | 2020-08-01,65.55,housing 63 | 2020-08-01,6.29,groceries 64 | 2020-08-01,25.23,groceries 65 | 2020-09-01,49.51,travel 66 | 2020-09-01,17.92,entertainment 67 | 2020-09-01,74.69,travel 68 | 2020-09-01,89.4,groceries 69 | 2020-09-01,55.0,travel 70 | 2020-09-01,81.93,utilities 71 | 2020-09-01,57.69,healthcare 72 | 2020-09-01,22.22,entertainment 73 | 2020-09-01,13.22,entertainment 74 | 2020-09-01,77.76,groceries 75 | 2020-09-01,13.22,groceries 76 | 2020-10-01,57.58,groceries 77 | 2020-10-01,4.0,car 78 | 2020-10-01,20.48,utilities 79 | 2020-10-01,15.74,utilities 80 | 2020-10-01,59.31,entertainment 81 | 2020-10-01,15.31,groceries 82 | 2020-10-01,96.55,groceries 83 | 2020-10-01,91.65,entertainment 84 | 2020-10-01,50.04,groceries 85 | 2020-10-01,80.78,groceries 86 | 2020-11-01,71.81,groceries 87 | 2020-11-01,24.68,housing 88 | 2020-11-01,69.69,healthcare 89 | 2020-11-01,56.34,groceries 90 | 2020-12-01,66.58,entertainment 91 | 2020-12-01,76.24,utilities 92 | 2020-12-01,81.17,groceries 93 | 2020-12-01,47.01,car 94 | 2020-12-01,71.57,groceries 95 | 2020-12-01,73.81,utilities 96 | 2020-12-01,9.22,healthcare 97 | 2020-12-01,32.22,travel 98 | 2020-12-01,5.72,entertainment 99 | 2020-12-01,73.53,groceries 100 | 2021-01-01,70.84,entertainment 101 | 2021-01-01,99.42,entertainment 102 | 2021-01-01,37.04,healthcare 103 | 2021-01-01,90.98,healthcare 104 | 2021-01-01,58.67,healthcare 105 | 2021-01-01,92.68,groceries 106 | 2021-01-01,65.82,groceries 107 | 2021-01-01,99.58,groceries 108 | 2021-01-01,16.65,healthcare 109 | 2021-01-01,10.93,groceries 110 | 2021-02-01,33.45,groceries 111 | 2021-02-01,70.84,travel 112 | 2021-02-01,34.73,entertainment 113 | 2021-02-01,46.42,groceries 114 | 2021-02-01,91.4,healthcare 115 | 2021-02-01,71.29,travel 116 | 2021-02-01,21.49,entertainment 117 | 2021-02-01,18.48,groceries 118 | 2021-02-01,55.53,healthcare 119 | 2021-02-01,77.15,groceries 120 | 2021-03-01,43.56,groceries 121 | 2021-03-01,83.79,healthcare 122 | 2021-03-01,84.38,travel 123 | 2021-03-01,24.06,groceries 124 | 2021-03-01,56.91,groceries 125 | 2021-03-01,9.95,travel 126 | 2021-03-01,17.04,housing 127 | 2021-03-01,65.6,entertainment 128 | 2021-03-01,48.31,housing 129 | 2021-03-01,53.55,healthcare 130 | 2021-04-01,85.99,car 131 | 2021-04-01,59.48,travel 132 | 2021-04-01,1.1,travel 133 | 2021-04-01,54.48,groceries 134 | 2021-04-01,14.25,car 135 | 2021-04-01,24.01,groceries 136 | 2021-04-01,66.32,healthcare 137 | 2021-04-01,38.34,healthcare 138 | 2021-04-01,62.12,groceries 139 | 2021-04-01,14.14,housing 140 | 2021-04-01,64.07,groceries 141 | 2021-04-01,97.5,housing 142 | 2021-04-01,97.26,entertainment 143 | 2021-04-01,90.99,entertainment 144 | 2021-05-01,64.94,car 145 | 2021-05-01,36.64,groceries 146 | 2021-05-01,70.0,groceries 147 | 2021-05-01,90.62,groceries 148 | 2021-05-01,92.65,groceries 149 | 2021-05-01,77.18,groceries 150 | 2021-05-01,57.91,groceries 151 | 2021-05-01,43.15,housing 152 | 2021-06-01,83.8,groceries 153 | 2021-06-01,49.62,entertainment 154 | 2021-06-01,71.06,groceries 155 | 2021-06-01,33.25,groceries 156 | 2021-06-01,53.4,groceries 157 | 2021-06-01,4.61,entertainment 158 | 2021-06-01,2.51,utilities 159 | 2021-06-01,56.95,entertainment 160 | 2021-06-01,82.12,travel 161 | 2021-06-01,29.88,entertainment 162 | 2021-06-01,1.36,utilities 163 | 2021-07-01,40.01,housing 164 | 2021-07-01,47.98,groceries 165 | 2021-07-01,45.75,groceries 166 | 2021-07-01,95.4,groceries 167 | 2021-07-01,90.35,groceries 168 | 2021-07-01,82.36,groceries 169 | 2021-07-01,92.92,car 170 | 2021-07-01,16.85,entertainment 171 | 2021-07-01,82.77,groceries 172 | 2021-07-01,63.85,utilities 173 | 2021-07-01,10.82,entertainment 174 | 2021-08-01,42.11,groceries 175 | 2021-08-01,10.76,groceries 176 | 2021-08-01,49.48,entertainment 177 | 2021-08-01,53.11,groceries 178 | 2021-08-01,51.99,groceries 179 | 2021-08-01,75.16,utilities 180 | 2021-08-01,53.6,groceries 181 | 2021-08-01,37.27,groceries 182 | 2021-08-01,11.23,groceries 183 | 2021-09-01,45.43,groceries 184 | 2021-09-01,73.96,groceries 185 | 2021-09-01,45.1,groceries 186 | 2021-09-01,25.35,groceries 187 | 2021-09-01,68.04,groceries 188 | 2021-09-01,62.09,groceries 189 | 2021-09-01,66.98,utilities 190 | 2021-09-01,32.28,groceries 191 | 2021-09-01,19.8,entertainment 192 | 2021-09-01,69.23,groceries 193 | 2021-10-01,56.28,healthcare 194 | 2021-10-01,2.16,entertainment 195 | 2021-10-01,59.04,groceries 196 | 2021-10-01,89.95,groceries 197 | 2021-10-01,35.4,groceries 198 | 2021-10-01,75.07,entertainment 199 | 2021-10-01,34.26,groceries 200 | 2021-11-01,95.83,groceries 201 | 2021-11-01,9.02,utilities 202 | 2021-11-01,85.69,healthcare 203 | 2021-11-01,24.64,healthcare 204 | 2021-11-01,49.18,healthcare 205 | 2021-11-01,53.08,entertainment 206 | 2021-11-01,29.73,travel 207 | 2021-11-01,17.31,groceries 208 | 2021-11-01,76.38,groceries 209 | 2021-11-01,89.54,groceries 210 | 2021-12-01,53.16,groceries 211 | 2021-12-01,53.25,entertainment 212 | 2021-12-01,91.38,car 213 | 2021-12-01,77.33,entertainment 214 | 2021-12-01,85.64,groceries 215 | 2021-12-01,61.59,entertainment 216 | 2021-12-01,43.79,car 217 | 2021-12-01,44.01,travel 218 | 2021-12-01,13.41,healthcare 219 | 2022-01-01,96.68,housing 220 | 2022-01-01,18.45,groceries 221 | 2022-01-01,78.89,groceries 222 | 2022-01-01,6.87,healthcare 223 | 2022-01-01,56.18,entertainment 224 | 2022-01-01,63.06,groceries 225 | 2022-01-01,15.97,groceries 226 | 2022-01-01,50.25,groceries 227 | 2022-01-01,58.76,groceries 228 | 2022-01-01,24.09,groceries 229 | 2022-01-01,31.04,groceries 230 | 2022-01-01,70.04,groceries 231 | 2022-01-01,25.06,groceries 232 | 2022-01-01,61.61,travel 233 | 2022-01-01,39.21,groceries 234 | 2022-01-01,11.46,groceries 235 | 2022-02-01,14.8,housing 236 | 2022-02-01,81.78,healthcare 237 | 2022-02-01,12.49,entertainment 238 | 2022-02-01,68.73,utilities 239 | 2022-02-01,30.66,groceries 240 | 2022-02-01,56.6,groceries 241 | 2022-02-01,73.0,entertainment 242 | 2022-02-01,7.76,travel 243 | 2022-03-01,22.03,groceries 244 | 2022-03-01,74.89,groceries 245 | 2022-03-01,27.95,groceries 246 | 2022-03-01,97.88,travel 247 | 2022-03-01,29.95,entertainment 248 | 2022-03-01,33.46,entertainment 249 | 2022-03-01,11.64,entertainment 250 | 2022-03-01,28.7,groceries 251 | 2022-03-01,84.11,groceries 252 | 2022-03-01,65.94,healthcare 253 | 2022-03-01,68.89,groceries 254 | 2022-03-01,99.26,car 255 | 2022-03-01,19.47,car 256 | 2022-03-01,79.93,entertainment 257 | 2022-04-01,81.24,groceries 258 | 2022-04-01,32.05,utilities 259 | 2022-04-01,74.43,utilities 260 | 2022-04-01,92.54,entertainment 261 | 2022-04-01,67.87,entertainment 262 | 2022-04-01,49.54,groceries 263 | 2022-04-01,72.31,entertainment 264 | 2022-04-01,74.85,groceries 265 | 2022-04-01,56.05,groceries 266 | 2022-04-01,69.44,travel 267 | 2022-04-01,85.56,groceries 268 | 2022-05-01,14.99,healthcare 269 | 2022-05-01,29.46,groceries 270 | 2022-05-01,17.41,groceries 271 | 2022-05-01,83.34,groceries 272 | 2022-05-01,40.07,groceries 273 | 2022-05-01,78.19,entertainment 274 | 2022-05-01,32.96,housing 275 | 2022-05-01,89.92,entertainment 276 | 2022-06-01,86.82,groceries 277 | 2022-06-01,69.78,entertainment 278 | 2022-06-01,66.43,groceries 279 | 2022-06-01,58.21,groceries 280 | 2022-06-01,73.24,groceries 281 | 2022-06-01,19.85,groceries 282 | 2022-06-01,47.97,housing 283 | -------------------------------------------------------------------------------- /part3/environment/README.md: -------------------------------------------------------------------------------- 1 | # Environment Setup 2 | 3 | ## Installation with Anaconda/Miniconda 4 | 5 | Run the two commands from the root directory. 6 | 7 | ```shell 8 | conda env create -f ./environment/conda.yaml 9 | conda activate dash-app-tutorial 10 | ``` 11 | 12 | ## Installation with Pip 13 | 14 | **Note:** Python >= 3.9 is required to use the [modern style](https://peps.python.org/pep-0585/) of type annotations. 15 | 16 | Run the command from the root directory. 17 | 18 | ```shell 19 | python -m pip install -r ./environment/requirements.txt 20 | ``` 21 | -------------------------------------------------------------------------------- /part3/environment/conda.yaml: -------------------------------------------------------------------------------- 1 | name: dash-app-tutorial 2 | dependencies: 3 | - python>=3.9 4 | - pip 5 | - pip: 6 | - -r requirements.txt 7 | -------------------------------------------------------------------------------- /part3/environment/requirements-dev.txt: -------------------------------------------------------------------------------- 1 | black 2 | pylint -------------------------------------------------------------------------------- /part3/environment/requirements.txt: -------------------------------------------------------------------------------- 1 | Babel 2 | dash 3 | dash-bootstrap-components 4 | pandas 5 | pandas-stubs 6 | plotly 7 | pyyaml 8 | python-i18n[YAML] -------------------------------------------------------------------------------- /part3/locale/category.en.yml: -------------------------------------------------------------------------------- 1 | en: 2 | car: Car 3 | entertainment: Entertainment 4 | groceries: Groceries 5 | healthcare: Medical/Healthcare 6 | housing: Mortgage/Rent 7 | travel: Vacation/Travel 8 | utilities: Utilities 9 | -------------------------------------------------------------------------------- /part3/locale/category.nl.yml: -------------------------------------------------------------------------------- 1 | nl: 2 | car: Auto 3 | entertainment: Vermaak 4 | groceries: Boodschappen 5 | healthcare: Gezondheid/zorg 6 | housing: Hypotheek/huur 7 | travel: Vakantie/reizen 8 | utilities: Voorzieningen 9 | -------------------------------------------------------------------------------- /part3/locale/general.en.yml: -------------------------------------------------------------------------------- 1 | --- 2 | en: 3 | amount: Amount 4 | app_title: Financial Dashboard 5 | category: Category 6 | month: Month 7 | no_data: No data selected 8 | select: Select... 9 | select_all: Select all 10 | year: Year 11 | -------------------------------------------------------------------------------- /part3/locale/general.nl.yml: -------------------------------------------------------------------------------- 1 | --- 2 | nl: 3 | amount: Bedrag 4 | app_title: Financieel Dashboard 5 | category: Categorie 6 | month: Maand 7 | no_data: Geen gegevens geselecteerd 8 | select: Selecteer... 9 | select_all: Selecteer alles 10 | year: Jaar 11 | -------------------------------------------------------------------------------- /part3/main.py: -------------------------------------------------------------------------------- 1 | import i18n 2 | from dash import Dash 3 | from dash_bootstrap_components.themes import BOOTSTRAP 4 | 5 | from src.components.layout import create_layout 6 | from src.data.loader import load_transaction_data 7 | from src.data.source import DataSource 8 | 9 | LOCALE = "nl" 10 | DATA_PATH = "./data/transactions.csv" 11 | 12 | 13 | def main() -> None: 14 | 15 | # set the locale and load the translations 16 | i18n.set("locale", LOCALE) 17 | i18n.load_path.append("locale") 18 | 19 | # load the data and create the data manager 20 | data = load_transaction_data(DATA_PATH, LOCALE) 21 | data = DataSource(data) 22 | 23 | app = Dash(external_stylesheets=[BOOTSTRAP]) 24 | app.title = i18n.t("general.app_title") 25 | app.layout = create_layout(app, data) 26 | app.run() 27 | 28 | 29 | if __name__ == "__main__": 30 | main() 31 | -------------------------------------------------------------------------------- /part3/src/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ArjanCodes/2022-dash/a0e56e00c98900485b0c75e5d5ce99b36ac5a2d2/part3/src/__init__.py -------------------------------------------------------------------------------- /part3/src/components/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ArjanCodes/2022-dash/a0e56e00c98900485b0c75e5d5ce99b36ac5a2d2/part3/src/components/__init__.py -------------------------------------------------------------------------------- /part3/src/components/bar_chart.py: -------------------------------------------------------------------------------- 1 | import i18n 2 | import plotly.express as px 3 | from dash import Dash, dcc, html 4 | from dash.dependencies import Input, Output 5 | 6 | from ..data.loader import DataSchema 7 | from ..data.source import DataSource 8 | from . import ids 9 | 10 | 11 | def render(app: Dash, source: DataSource) -> html.Div: 12 | @app.callback( 13 | Output(ids.BAR_CHART, "children"), 14 | [ 15 | Input(ids.YEAR_DROPDOWN, "value"), 16 | Input(ids.MONTH_DROPDOWN, "value"), 17 | Input(ids.CATEGORY_DROPDOWN, "value"), 18 | ], 19 | ) 20 | def update_bar_chart( 21 | years: list[str], months: list[str], categories: list[str] 22 | ) -> html.Div: 23 | filtered_source = source.filter(years, months, categories) 24 | if not filtered_source.row_count: 25 | return html.Div(i18n.t("general.no_data"), id=ids.BAR_CHART) 26 | 27 | fig = px.bar( 28 | filtered_source.create_pivot_table(), 29 | x=DataSchema.CATEGORY, 30 | y=DataSchema.AMOUNT, 31 | color="category", 32 | labels={ 33 | "category": i18n.t("general.category"), 34 | "amount": i18n.t("general.amount"), 35 | }, 36 | ) 37 | 38 | return html.Div(dcc.Graph(figure=fig), id=ids.BAR_CHART) 39 | 40 | return html.Div(id=ids.BAR_CHART) 41 | -------------------------------------------------------------------------------- /part3/src/components/category_dropdown.py: -------------------------------------------------------------------------------- 1 | import i18n 2 | from dash import Dash, dcc, html 3 | from dash.dependencies import Input, Output 4 | 5 | from ..data.source import DataSource 6 | from . import ids 7 | from .dropdown_helper import to_dropdown_options 8 | 9 | 10 | def render(app: Dash, source: DataSource) -> html.Div: 11 | @app.callback( 12 | Output(ids.CATEGORY_DROPDOWN, "value"), 13 | [ 14 | Input(ids.YEAR_DROPDOWN, "value"), 15 | Input(ids.MONTH_DROPDOWN, "value"), 16 | Input(ids.SELECT_ALL_CATEGORIES_BUTTON, "n_clicks"), 17 | ], 18 | ) 19 | def select_all_categories(years: list[str], months: list[str], _: int) -> list[str]: 20 | return source.filter(years=years, months=months).unique_categories 21 | 22 | return html.Div( 23 | children=[ 24 | html.H6(i18n.t("general.category")), 25 | dcc.Dropdown( 26 | id=ids.CATEGORY_DROPDOWN, 27 | options=to_dropdown_options(source.unique_categories), 28 | value=source.unique_categories, 29 | multi=True, 30 | placeholder=i18n.t("general.select"), 31 | ), 32 | html.Button( 33 | className="dropdown-button", 34 | children=[i18n.t("general.select_all")], 35 | id=ids.SELECT_ALL_CATEGORIES_BUTTON, 36 | n_clicks=0, 37 | ), 38 | ], 39 | ) 40 | -------------------------------------------------------------------------------- /part3/src/components/dropdown_helper.py: -------------------------------------------------------------------------------- 1 | def to_dropdown_options(values: list[str]) -> list[dict[str, str]]: 2 | return [{"label": value, "value": value} for value in values] 3 | -------------------------------------------------------------------------------- /part3/src/components/ids.py: -------------------------------------------------------------------------------- 1 | BAR_CHART = "bar-chart" 2 | PIE_CHART = "pie-chart" 3 | 4 | SELECT_ALL_CATEGORIES_BUTTON = "select-all-categories-button" 5 | CATEGORY_DROPDOWN = "category-dropdown" 6 | 7 | SELECT_ALL_MONTHS_BUTTON = "select-all-months-button" 8 | MONTH_DROPDOWN = "month-dropdown" 9 | 10 | YEAR_DROPDOWN = "year-dropdown" 11 | SELECT_ALL_YEARS_BUTTON = "select-all-years-button" 12 | -------------------------------------------------------------------------------- /part3/src/components/layout.py: -------------------------------------------------------------------------------- 1 | from dash import Dash, html 2 | from src.components import ( 3 | bar_chart, 4 | category_dropdown, 5 | month_dropdown, 6 | pie_chart, 7 | year_dropdown, 8 | ) 9 | 10 | from ..data.source import DataSource 11 | 12 | 13 | def create_layout(app: Dash, source: DataSource) -> html.Div: 14 | return html.Div( 15 | className="app-div", 16 | children=[ 17 | html.H1(app.title), 18 | html.Hr(), 19 | html.Div( 20 | className="dropdown-container", 21 | children=[ 22 | year_dropdown.render(app, source), 23 | month_dropdown.render(app, source), 24 | category_dropdown.render(app, source), 25 | ], 26 | ), 27 | bar_chart.render(app, source), 28 | pie_chart.render(app, source), 29 | ], 30 | ) 31 | -------------------------------------------------------------------------------- /part3/src/components/month_dropdown.py: -------------------------------------------------------------------------------- 1 | import i18n 2 | from dash import Dash, dcc, html 3 | from dash.dependencies import Input, Output 4 | 5 | from ..data.source import DataSource 6 | from . import ids 7 | from .dropdown_helper import to_dropdown_options 8 | 9 | 10 | def render(app: Dash, source: DataSource) -> html.Div: 11 | @app.callback( 12 | Output(ids.MONTH_DROPDOWN, "value"), 13 | [ 14 | Input(ids.YEAR_DROPDOWN, "value"), 15 | Input(ids.SELECT_ALL_MONTHS_BUTTON, "n_clicks"), 16 | ], 17 | ) 18 | def select_all_months(years: list[str], _: int) -> list[str]: 19 | return source.filter(years=years).unique_months 20 | 21 | return html.Div( 22 | children=[ 23 | html.H6(i18n.t("general.month")), 24 | dcc.Dropdown( 25 | id=ids.MONTH_DROPDOWN, 26 | options=to_dropdown_options(source.unique_months), 27 | value=source.unique_months, 28 | multi=True, 29 | ), 30 | html.Button( 31 | className="dropdown-button", 32 | children=[i18n.t("general.select_all")], 33 | id=ids.SELECT_ALL_MONTHS_BUTTON, 34 | n_clicks=0, 35 | ), 36 | ] 37 | ) 38 | -------------------------------------------------------------------------------- /part3/src/components/pie_chart.py: -------------------------------------------------------------------------------- 1 | import i18n 2 | import plotly.graph_objects as go 3 | from dash import Dash, dcc, html 4 | from dash.dependencies import Input, Output 5 | 6 | from ..data.source import DataSource 7 | from . import ids 8 | 9 | 10 | def render(app: Dash, source: DataSource) -> html.Div: 11 | @app.callback( 12 | Output(ids.PIE_CHART, "children"), 13 | [ 14 | Input(ids.YEAR_DROPDOWN, "value"), 15 | Input(ids.MONTH_DROPDOWN, "value"), 16 | Input(ids.CATEGORY_DROPDOWN, "value"), 17 | ], 18 | ) 19 | def update_pie_chart( 20 | years: list[str], months: list[str], categories: list[str] 21 | ) -> html.Div: 22 | filtered_source = source.filter(years, months, categories) 23 | if not filtered_source.row_count: 24 | return html.Div(i18n.t("general.no_data"), id=ids.PIE_CHART) 25 | 26 | pie = go.Pie( 27 | labels=filtered_source.all_categories, 28 | values=filtered_source.all_amounts, 29 | hole=0.5, 30 | ) 31 | 32 | fig = go.Figure(data=[pie]) 33 | fig.update_layout(margin={"t": 40, "b": 0, "l": 0, "r": 0}) 34 | fig.update_traces(hovertemplate="%{label}
$%{value:.2f}") 35 | 36 | return html.Div(dcc.Graph(figure=fig), id=ids.PIE_CHART) 37 | 38 | return html.Div(id=ids.PIE_CHART) 39 | -------------------------------------------------------------------------------- /part3/src/components/year_dropdown.py: -------------------------------------------------------------------------------- 1 | import i18n 2 | from dash import Dash, dcc, html 3 | from dash.dependencies import Input, Output 4 | 5 | from ..data.source import DataSource 6 | from . import ids 7 | from .dropdown_helper import to_dropdown_options 8 | 9 | 10 | def render(app: Dash, source: DataSource) -> html.Div: 11 | @app.callback( 12 | Output(ids.YEAR_DROPDOWN, "value"), 13 | Input(ids.SELECT_ALL_YEARS_BUTTON, "n_clicks"), 14 | ) 15 | def select_all_years(_: int) -> list[str]: 16 | return source.unique_years 17 | 18 | return html.Div( 19 | children=[ 20 | html.H6(i18n.t("general.year")), 21 | dcc.Dropdown( 22 | id=ids.YEAR_DROPDOWN, 23 | options=to_dropdown_options(source.unique_years), 24 | value=source.unique_years, 25 | multi=True, 26 | ), 27 | html.Button( 28 | className="dropdown-button", 29 | children=[i18n.t("general.select_all")], 30 | id=ids.SELECT_ALL_YEARS_BUTTON, 31 | n_clicks=0, 32 | ), 33 | ] 34 | ) 35 | -------------------------------------------------------------------------------- /part3/src/data/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ArjanCodes/2022-dash/a0e56e00c98900485b0c75e5d5ce99b36ac5a2d2/part3/src/data/__init__.py -------------------------------------------------------------------------------- /part3/src/data/loader.py: -------------------------------------------------------------------------------- 1 | import datetime as dt 2 | from functools import partial, reduce 3 | from typing import Callable 4 | 5 | import babel.dates 6 | import i18n 7 | import pandas as pd 8 | 9 | Preprocessor = Callable[[pd.DataFrame], pd.DataFrame] 10 | 11 | 12 | class DataSchema: 13 | AMOUNT = "amount" 14 | CATEGORY = "category" 15 | DATE = "date" 16 | MONTH = "month" 17 | YEAR = "year" 18 | 19 | 20 | def create_year_column(df: pd.DataFrame) -> pd.DataFrame: 21 | df[DataSchema.YEAR] = df[DataSchema.DATE].dt.year.astype(str) 22 | return df 23 | 24 | 25 | def create_month_column(df: pd.DataFrame) -> pd.DataFrame: 26 | df[DataSchema.MONTH] = df[DataSchema.DATE].dt.month.astype(str) 27 | return df 28 | 29 | 30 | def convert_date_locale(df: pd.DataFrame, locale: str) -> pd.DataFrame: 31 | def date_repr(date: dt.date) -> str: 32 | return babel.dates.format_date(date, format="MMMM", locale=locale) 33 | 34 | df[DataSchema.MONTH] = df[DataSchema.DATE].apply(date_repr) 35 | return df 36 | 37 | 38 | def translate_category_language(df: pd.DataFrame) -> pd.DataFrame: 39 | def translate(category: str) -> str: 40 | return i18n.t(f"category.{category}") 41 | 42 | df[DataSchema.CATEGORY] = df[DataSchema.CATEGORY].apply(translate) 43 | return df 44 | 45 | 46 | def compose(*functions: Preprocessor) -> Preprocessor: 47 | return reduce(lambda f, g: lambda x: g(f(x)), functions) 48 | 49 | 50 | def load_transaction_data(path: str, locale: str) -> pd.DataFrame: 51 | # load the data from the CSV file 52 | data = pd.read_csv( 53 | path, 54 | dtype={ 55 | DataSchema.AMOUNT: float, 56 | DataSchema.CATEGORY: str, 57 | DataSchema.DATE: str, 58 | }, 59 | parse_dates=[DataSchema.DATE], 60 | ) 61 | preprocessor = compose( 62 | create_year_column, 63 | create_month_column, 64 | partial(convert_date_locale, locale=locale), 65 | translate_category_language, 66 | ) 67 | return preprocessor(data) 68 | -------------------------------------------------------------------------------- /part3/src/data/source.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from dataclasses import dataclass 4 | from typing import Optional 5 | 6 | import pandas as pd 7 | 8 | from ..data.loader import DataSchema 9 | from .loader import DataSchema 10 | 11 | 12 | @dataclass 13 | class DataSource: 14 | _data: pd.DataFrame 15 | 16 | def filter( 17 | self, 18 | years: Optional[list[str]] = None, 19 | months: Optional[list[str]] = None, 20 | categories: Optional[list[str]] = None, 21 | ) -> DataSource: 22 | if years is None: 23 | years = self.unique_years 24 | if months is None: 25 | months = self.unique_months 26 | if categories is None: 27 | categories = self.unique_categories 28 | filtered_data = self._data.query( 29 | "year in @years and month in @months and category in @categories" 30 | ) 31 | return DataSource(filtered_data) 32 | 33 | def create_pivot_table(self) -> pd.DataFrame: 34 | pt = self._data.pivot_table( 35 | values=DataSchema.AMOUNT, 36 | index=[DataSchema.CATEGORY], 37 | aggfunc="sum", 38 | fill_value=0, 39 | dropna=False, 40 | ) 41 | return pt.reset_index().sort_values(DataSchema.AMOUNT, ascending=False) 42 | 43 | @property 44 | def row_count(self) -> int: 45 | return self._data.shape[0] 46 | 47 | @property 48 | def all_years(self) -> list[str]: 49 | return self._data[DataSchema.YEAR].tolist() 50 | 51 | @property 52 | def all_months(self) -> list[str]: 53 | return self._data[DataSchema.MONTH].tolist() 54 | 55 | @property 56 | def all_categories(self) -> list[str]: 57 | return self._data[DataSchema.CATEGORY].tolist() 58 | 59 | @property 60 | def all_amounts(self) -> list[str]: 61 | return self._data[DataSchema.AMOUNT].tolist() 62 | 63 | @property 64 | def unique_years(self) -> list[str]: 65 | return sorted(set(self.all_years), key=int) 66 | 67 | @property 68 | def unique_months(self) -> list[str]: 69 | return sorted(set(self.all_months)) 70 | 71 | @property 72 | def unique_categories(self) -> list[str]: 73 | return sorted(set(self.all_categories)) 74 | --------------------------------------------------------------------------------