diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c00f9fa --- /dev/null +++ b/.gitignore @@ -0,0 +1,167 @@ +# This file lists the files and folders that should NOT be committed to GitHub. + +# Virtual environment +.venv/ +venv/ + + +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..6ba1afd --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,6 @@ +{ + "[python]": { + "editor.defaultFormatter": "ms-python.black-formatter" + }, + "python.formatting.provider": "none" +} \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..15e1ce9 --- /dev/null +++ b/README.md @@ -0,0 +1,70 @@ +# Continuous Intelligence and Interactive Analytics - Initial App + +- Repository: [cintel-04-reactive](https://github.com/denisecase/cintel-04-reactive) +- Website: [cintel-04-reactive](https://denisecase.github.io/cintel-04-reactive/) +- Author: [Denise Case](https://github.com/denisecase) + + +Note: 🚀 Rocket Tips are for learners who want to go beyond the basics. +They are NOT required but can improve our workflow, increase productivity, and make things more enjoyable. +----- + + +## Prerequisites + +You'll need the tools installed in + +- [cintel-01-getting-started](https://github.com/denisecase/cintel-01-getting-started) +- [cintel-02-app](https://github.com/denisecase/cintel-02-app) +- [cintel-03-data](https://github.com/denisecase/cintel-03-data) + +## Sign Up for shinyapps.io (Free Account) + +Sign up for a free account on shinyapps.io. +I sign in via GitHub for convenience. + +## Copy This Repository + +Copy this starter repository into your own GitHub account by clicking the 'Fork' button at the top of this page. + +----- + +## Customize Your Web App + +### Get the Code to your Local Machine + +1. Open VS Code and from the menu, select **View** / **Command Palette**. +1. Type "Git: Clone" in the command palette and select it. +1. Enter the URL (web address) of your forked GitHub repository (make sure it contains your GitHub username - not denisecase). +1. Choose a directory on your local machine (e.g., Documents folder) to store the project. +1. If prompted, sign in to GitHub from VS Code. + +### Make Changes in VS Code + +With your respository folder open in VS Code: + +1. Click on this README.md file for editing. +1. Update the README.md file by changing your name in the author link above. +1. Update the links in the README.md file to your username instead of denisecase. + +### Save Your Changes + +1. After making changes, you want to send them back to GitHub. +1. In VS Code, find the "Source Control" icon and click it. +1. Important: Enter a brief commit message describing your changes. +1. Change the "Commit" button dropdown to "Commit and Push" to send your changes back to GitHub. + +----- + +## Reminders + +Details matter - check spelling, capitalization, plurals, spacing when things don't match. + +## Resources + +- [Seaborn](https://seaborn.pydata.org/) +- [Plotly](https://plotly.com/python/) +- [Bokeh](https://docs.bokeh.org/en/latest/index.html) + + +- [Seaborn Flights Dataset](https://seaborn.pydata.org/tutorial/data_structure.html) \ No newline at end of file diff --git a/REQUIREMENTS.md b/REQUIREMENTS.md new file mode 100644 index 0000000..a866585 --- /dev/null +++ b/REQUIREMENTS.md @@ -0,0 +1,34 @@ +# Python Libraries for Interactive Analytics + +This collection includes various Python packages that are used for interactive analytics. Each tool serves a unique purpose and is suitable for different use cases. +Use your favorites. Try new ones. Share your experiences with the community. + +Data Manipulation: +- **pandas**: A powerful data manipulation library that provides flexible data structures and functions to manipulate structured data. [pandas documentation](https://pandas.pydata.org/) + +Excel Library: +- **openpyxl**: A Python library to read/write Excel 2010 xlsx/xlsm/xltx/xltm files. [openpyxl documentation](https://openpyxl.readthedocs.io/en/stable/) + +Template Engine: +- **jinja2**: A modern and designer-friendly templating language for Python. It is often used to create HTML, XML or other markup formats that are returned to the user via an HTTP request. [jinja2 documentation](https://jinja.palletsprojects.com/) + +Data Visualization: +- **matplotlib**: A data visualization library that provides a MATLAB-like interface for creating plots and charts. [matplotlib dataset list](https://matplotlib.org/stable/api/_as_gen/matplotlib.pyplot.html#module-matplotlib.pyplot) +- **seaborn**: A data visualization library built on top of matplotlib, providing datasets and high-level statistical graphics. [seaborn dataset list](https://github.com/mwaskom/seaborn-data) +- **plotnine**: A Python implementation of the R package ggplot2, which is a "grammar of graphics" implementation. It allows you to create complex plots with a few lines of code. [plotnine documentation](https://plotnine.readthedocs.io/en/stable/) +- **plotly**: A Python graphing library that makes interactive, publication-quality graphs online. It provides a wide range of visualization types and features. [plotly documentation](https://plotly.com/python/) +- **holoviews**: A Python library that makes data visualization as simple as possible, by automatically connecting data and its graphical representation. [holoviews documentation](http://holoviews.org/) +- **hvplot**: A high-level plotting API for pandas, dask, xarray, and networkx built on HoloViews. [hvplot documentation](https://hvplot.holoviz.org/) + +Interactive Maps: +- **ipyleaflet**: A Python library for creating interactive maps. It is a Jupyter notebook widget that uses leaflet.js for rendering maps. [ipyleaflet documentation](https://ipyleaflet.readthedocs.io/en/latest/) + +Web-based Visualization: +- **bokeh**: A Python library for creating interactive visualizations and plots, emphasizing web-based, modern, and high-performance output. [bokeh documentation](https://docs.bokeh.org/en/latest/index.html) +- **jupyter_bokeh**: A Jupyter extension for rendering bokeh content in Jupyter notebook cells. [jupyter_bokeh documentation](https://github.com/bokeh/jupyter_bokeh) + +Web Applications: +- **shiny**: A package in R (with a Python version also available) for creating interactive web applications straight from R and Python. [shiny documentation](https://shiny.rstudio.com/) +- **shinyswatch**: A package that provides additional themes for Shiny applications. [shinyswatch documentation](https://github.com/Appsilon/shinyswatch) +- **shinywidgets**: Enhance your Shiny apps with advanced widgets like sliders, checkboxes, etc. [shinywidgets documentation](https://github.com/Appsilon/shiny.widgets) +- **panel**: A high-level app anddashboarding solution for Python that works well with the PyData ecosystem. It allows you to create interactive dashboards and applications with ease. [panel documentation](https://panel.holoviz.org/) diff --git a/SHINY.md b/SHINY.md new file mode 100644 index 0000000..13f982b --- /dev/null +++ b/SHINY.md @@ -0,0 +1,92 @@ +# Interactive Analytics with Shiny + +Earlier, you installed Python and verified it with: + +```shell +python --version +python3 --version +py --version +``` + +Use the command that works instead of `python` in the following instructions. +You're encouraged to modify these Markdown (.md) files to reflect the commands that work on your machine. + +## Install and Upgrade Python Tools Globally + +Install some additional content into your global Python for best results. +open your terminal (PowerShell on Windows, Terminal on Mac/Linux) and +install these to your default Python using the commands below. + + +```shell +python -m pip install --upgrade pip wheel +python -m pip install --upgrade black ruff pyright +python -m pip install --upgrade rsconnect-python +``` + +You must have reconnect-python installed for the next step. + +## Authorize shinyapps.io + +Using Chrome (recommended), sign in to your free shinyapps.io account (I use GitHub to sign in.) + +1. On the Getting Started page, click on the "Start with Python" tab. +1. Click "Show Secret" +1. Click "Copy to Clipboard". Follow the instructions. Mine said +1. Hit Ctrl c / ENTER to copy the provided command to the clipboard. +1. Open a terminal window. (Terminal on Mac/Linux, PowerShell on Windows). +1. Click in the terminal window to paste the command and hit ENTER to run it. + +![Get the Command to Authorize shinyapps.io](images/GetCommandToAuthorizeShinyAppsdotIO.PNG) + +## Create a Virtual Environment + +```shell +python -m venv .venv +``` + +When VS Code asks if it should add the new virtual environment, click yes. + + +## Activate the Virtual Environment + +- Activate it on Windows: `.venv\Scripts\activate` +- Activate it on macOS/Linux `source .venv/bin/` + +## Install Libaries into Virtual Environment + +This gives a good selection of options. You only need to install the ones you use. + +```shell +python -m pip install --upgrade pip wheel shiny shinyswatch +python -m pip install --upgrade pandas openpyxl jinja2 matplotlib seaborn plotnine +python -m pip install --upgrade shinywidgets plotly holoviews panel hvplot ipyleaflet +python -m pip install --upgrade jupyter_bokeh + +``` + +OR List your requirements in requirements.txt and install them all at once. + +```shell +python -m pip install --upgrade -r requirements.txt +``` + + +## Run the App + +Verify your virtual environment is activated. Run the app. + +```shell +shiny run --reload app.py +``` + +Open the app by following the instructions provided in the terminal. +For example, try CRTL CLICK (at the same time) on the URL displayed (http://127.0.0.1:8000). + +Hit CTRL c (at the same time) to quit the app. +If it won't stop, close the terminal window. +Reopen the terminal window and be sure the virtual environment is activated +before running the app again. + + + diff --git a/app.py b/app.py new file mode 100644 index 0000000..1efc131 --- /dev/null +++ b/app.py @@ -0,0 +1,78 @@ +""" +Purpose: Use Python to create a continuous intelligence and +interactive analytics dashboard using Shiny for Python and +interactive charts from Holoviews Bokeh and Plotly Express. + +""" +from shiny import * +import shinyswatch + +from flights_server import get_flights_server_functions +from flights_ui_inputs import get_flights_sidebar +from flights_ui_outputs import get_flights_main + +from mtcars_server import get_mtcars_server_functions +from mtcars_ui_inputs import get_mtcars_sidebar +from mtcars_ui_outputs import get_mtcars_main + +from penguins_server import get_penguins_server_functions +from penguins_ui_inputs import get_penguins_sidebar +from penguins_ui_outputs import get_penguins_main + +from relationships_server import get_relationships_server_functions +from relationships_ui_inputs import get_relationships_sidebar +from relationships_ui_outputs import get_relationships_main + +from util_logger import setup_logger + +logger, logname = setup_logger(__name__) + +app_ui = ui.page_navbar( + shinyswatch.theme.minty(), + ui.nav( + "Flights", + ui.layout_sidebar( + get_flights_sidebar(), + get_flights_main(), + ), + ), + ui.nav( + "MT_Cars", + ui.layout_sidebar( + get_mtcars_sidebar(), + get_mtcars_main(), + ), + ), + ui.nav( + "Penguins", + ui.layout_sidebar( + get_penguins_sidebar(), + get_penguins_main(), + ), + ), + ui.nav( + "Relationships", + ui.layout_sidebar( + get_relationships_sidebar(), + get_relationships_main(), + ), + ), + + ui.nav(ui.a("About", href="https://github.com/denisecase")), + ui.nav(ui.a("GitHub", href="https://github.com/denisecase/cintel-04-reactive")), + ui.nav(ui.a("App", href="https://denisecase.github.io/cintel-04-reactive/")), + ui.nav(ui.a("Examples", href="https://shinylive.io/py/examples/")), + ui.nav(ui.a("shinywidgets", href="https://shiny.rstudio.com/py/docs/ipywidgets.html")), + title=ui.h1("Case Dashboard"), +) + + +def server(input, output, session): + logger.info("Starting server...") + flights_server_functions = get_flights_server_functions(input, output, session) + mtcars_server_functions = get_mtcars_server_functions(input, output, session) + penguins_server_functions = get_penguins_server_functions(input, output, session) + relationships_server_functions = get_relationships_server_functions(input, output, session) + +#app = App(app_ui, server, debug=True) +app = App(app_ui, server) diff --git a/app_data.py b/app_data.py new file mode 100644 index 0000000..683a9c7 --- /dev/null +++ b/app_data.py @@ -0,0 +1,42 @@ +''' +Purpose: Use Python to create local datasets for review. + +Once they exist, you don't need to run these, but the examples may be +useful for creating your own datasets. + +You can uncomment the code and add it to your app.py file. +After the dataset is created, comment out the code again. + +You can also run this file by itself to create datasets. + +mtcars.csv - from R mtcars dataset +https://shinylive.io/py/examples/#read-local-csv + +penguins - from Seaborn penguins dataset + +flights - from Seaborn flights dataset + +Note: A local csv copy allows review from within VS Code. +''' + +import pandas as pd +import seaborn as sns +from bokeh.sampledata.les_mis import data as les_mis_data +from bokeh.sampledata.movies_data import data as movies_data + + +# penguins = sns.load_dataset("penguins") +# penguins.to_excel("penguins.xlsx") +#penguins.to_csv("penguins.csv") + + +# flights = sns.load_dataset("flights") +# flights.to_excel("flights.xlsx") +# flights.to_csv("flights.csv") + +les_mis = pd.DataFrame(les_mis_data['links']) +les_mis.to_excel("les_mis.xlsx") +les_mis.to_csv("les_mis.csv") + + + diff --git a/flights.csv b/flights.csv new file mode 100644 index 0000000..a461317 --- /dev/null +++ b/flights.csv @@ -0,0 +1,145 @@ +,year,month,passengers +0,1949,Jan,112 +1,1949,Feb,118 +2,1949,Mar,132 +3,1949,Apr,129 +4,1949,May,121 +5,1949,Jun,135 +6,1949,Jul,148 +7,1949,Aug,148 +8,1949,Sep,136 +9,1949,Oct,119 +10,1949,Nov,104 +11,1949,Dec,118 +12,1950,Jan,115 +13,1950,Feb,126 +14,1950,Mar,141 +15,1950,Apr,135 +16,1950,May,125 +17,1950,Jun,149 +18,1950,Jul,170 +19,1950,Aug,170 +20,1950,Sep,158 +21,1950,Oct,133 +22,1950,Nov,114 +23,1950,Dec,140 +24,1951,Jan,145 +25,1951,Feb,150 +26,1951,Mar,178 +27,1951,Apr,163 +28,1951,May,172 +29,1951,Jun,178 +30,1951,Jul,199 +31,1951,Aug,199 +32,1951,Sep,184 +33,1951,Oct,162 +34,1951,Nov,146 +35,1951,Dec,166 +36,1952,Jan,171 +37,1952,Feb,180 +38,1952,Mar,193 +39,1952,Apr,181 +40,1952,May,183 +41,1952,Jun,218 +42,1952,Jul,230 +43,1952,Aug,242 +44,1952,Sep,209 +45,1952,Oct,191 +46,1952,Nov,172 +47,1952,Dec,194 +48,1953,Jan,196 +49,1953,Feb,196 +50,1953,Mar,236 +51,1953,Apr,235 +52,1953,May,229 +53,1953,Jun,243 +54,1953,Jul,264 +55,1953,Aug,272 +56,1953,Sep,237 +57,1953,Oct,211 +58,1953,Nov,180 +59,1953,Dec,201 +60,1954,Jan,204 +61,1954,Feb,188 +62,1954,Mar,235 +63,1954,Apr,227 +64,1954,May,234 +65,1954,Jun,264 +66,1954,Jul,302 +67,1954,Aug,293 +68,1954,Sep,259 +69,1954,Oct,229 +70,1954,Nov,203 +71,1954,Dec,229 +72,1955,Jan,242 +73,1955,Feb,233 +74,1955,Mar,267 +75,1955,Apr,269 +76,1955,May,270 +77,1955,Jun,315 +78,1955,Jul,364 +79,1955,Aug,347 +80,1955,Sep,312 +81,1955,Oct,274 +82,1955,Nov,237 +83,1955,Dec,278 +84,1956,Jan,284 +85,1956,Feb,277 +86,1956,Mar,317 +87,1956,Apr,313 +88,1956,May,318 +89,1956,Jun,374 +90,1956,Jul,413 +91,1956,Aug,405 +92,1956,Sep,355 +93,1956,Oct,306 +94,1956,Nov,271 +95,1956,Dec,306 +96,1957,Jan,315 +97,1957,Feb,301 +98,1957,Mar,356 +99,1957,Apr,348 +100,1957,May,355 +101,1957,Jun,422 +102,1957,Jul,465 +103,1957,Aug,467 +104,1957,Sep,404 +105,1957,Oct,347 +106,1957,Nov,305 +107,1957,Dec,336 +108,1958,Jan,340 +109,1958,Feb,318 +110,1958,Mar,362 +111,1958,Apr,348 +112,1958,May,363 +113,1958,Jun,435 +114,1958,Jul,491 +115,1958,Aug,505 +116,1958,Sep,404 +117,1958,Oct,359 +118,1958,Nov,310 +119,1958,Dec,337 +120,1959,Jan,360 +121,1959,Feb,342 +122,1959,Mar,406 +123,1959,Apr,396 +124,1959,May,420 +125,1959,Jun,472 +126,1959,Jul,548 +127,1959,Aug,559 +128,1959,Sep,463 +129,1959,Oct,407 +130,1959,Nov,362 +131,1959,Dec,405 +132,1960,Jan,417 +133,1960,Feb,391 +134,1960,Mar,419 +135,1960,Apr,461 +136,1960,May,472 +137,1960,Jun,535 +138,1960,Jul,622 +139,1960,Aug,606 +140,1960,Sep,508 +141,1960,Oct,461 +142,1960,Nov,390 +143,1960,Dec,432 diff --git a/flights.xlsx b/flights.xlsx new file mode 100644 index 0000000..4c70567 Binary files /dev/null and b/flights.xlsx differ diff --git a/flights_server.py b/flights_server.py new file mode 100644 index 0000000..a7e7dbe --- /dev/null +++ b/flights_server.py @@ -0,0 +1,113 @@ +""" +Purpose: Provide reactive output for the Flights dataset. + +Use inputs from the UI Sidebar to filter the dataset. + +Update ouputs in the UI Main Panel. + +Matching the IDs in the UI Sidebar and function/ouput names in the UI Main Panel +to this server code is critical. They are case sensitive and must match exactly. + +This example uses dates - arguably the most complex UI input type. + - Consider leap years, time changes, daylight savings, etc. + - Working with dates is often challenging. + - This example is a good starting point - there are many examples online. + +""" +from shiny import * +import pandas as pd +from shinywidgets import render_widget +import plotly.express as px +import plotly.graph_objs as go + +from util_logger import setup_logger +logger, logname = setup_logger(__name__) + +def get_flights_server_functions(input, output, session): + + original_df = pd.read_excel("flights.xlsx") + + # create new field with year as a string and month together + original_df['year-mon'] = original_df['year'].astype(str) + '-' + original_df['month'] + total_count = len(original_df) + + reactive_df = reactive.Value() + + @reactive.Effect + @reactive.event(input.FLIGHTS_DATE_RANGE) + def _(): + ''' Reactive effect to update the filtered dataframe when inputs change. + It doesn't need a name, because no one calls it directly.''' + + logger.info("UI inputs changed. Updating flights reactive df") + + df = original_df.copy() + + # first, drop the unnamed index column + # axis=1 means drop a column, axis=0 means drop a row + df = df.drop(df.columns[0], axis=1) + + # Create a new column using pd.to_datetime() method for filtering + year_string = df["year"].astype(str) + month_string = df["month"].astype(str) + df["Date"] = pd.to_datetime(year_string + "-" + month_string) + + # Convert datetime to just the date, no time (datetime objects have both) + df["Date"] = df["Date"].dt.date + + input_range = input.FLIGHTS_DATE_RANGE() + input_min = input_range[0] + input_max = input_range[1] + df = df[(df["Date"] >= input_min) & (df["Date"] <= input_max)] + + #logger.debug(f"filtered flights df: {df}") + reactive_df.set(df) + + + @output + @render.text + def flights_record_count_string(): + logger.debug("Triggered: flights_filter_record_count_string") + filtered_count = len(reactive_df.get()) + message = f"Showing {filtered_count} of {total_count} records" + logger.debug(f"filter message: {message}") + return message + + @output + @render.table + def flights_filtered_table(): + filtered_df = reactive_df.get() + filtered_df.drop(columns=["Date"], inplace=True) + return filtered_df + + @output + @render_widget + def flights_output_widget1(): + df = reactive_df.get() + px_plot = px.scatter(df, + x="year", + y="passengers", + title="Flights Scatter Chart (Plotly Express)", + color="month" + ) + return px_plot + + @output + @render_widget + def flights_output_widget2(): + df = reactive_df.get() + + px_plot = px.line(df, + x="year-mon", + y="passengers", + title="Flights Line Chart (Plotly Express)", + labels={"year-mon": "Year-Mon", "passengers": "Passengers"}) + return px_plot + + # return a list of function names for use in reactive outputs + return [ + flights_record_count_string, + flights_filtered_table, + flights_output_widget1, + flights_output_widget2, + ] diff --git a/flights_ui_inputs.py b/flights_ui_inputs.py new file mode 100644 index 0000000..94127e6 --- /dev/null +++ b/flights_ui_inputs.py @@ -0,0 +1,32 @@ +""" +Purpose: Provide user interaction options for the Flights dataset. + +IDs must be unique. They are capitalized in this app for clarity (not typical). +The IDs are case-sensitive and must match the server code exactly. + +We prefaced IDs with the dataset name to avoid naming conflicts. + +See +Example date range at https://shinylive.io/py/examples/#date-range-input +Function ref at: https://shiny.rstudio.com/py/api/ui.update_date_range.html + + +""" +from datetime import date +from shiny import ui + + +def get_flights_sidebar(): + return ui.panel_sidebar( + ui.h2("Flights Interaction"), + ui.tags.hr(), + ui.input_date_range( + "FLIGHTS_DATE_RANGE", + "Enter Date Range", + start=date(1949, 1, 1), + end=date(1960, 12, 31), + ), + ui.tags.hr(), + ui.p("Please be patient. The charts may take a few seconds to load."), + ui.tags.hr(), + ) diff --git a/flights_ui_outputs.py b/flights_ui_outputs.py new file mode 100644 index 0000000..58a6954 --- /dev/null +++ b/flights_ui_outputs.py @@ -0,0 +1,22 @@ +''' +Purpose: Display ouput for the Flights dataset. +''' + +from shiny import ui +from shinywidgets import output_widget + +def get_flights_main(): + return ui.panel_main( + ui.h2("Main Panel with Reactive Output"), + ui.tags.hr(), + ui.tags.section( + ui.h3("Filtered Flights: Charts"), + output_widget("flights_output_widget1"), + output_widget("flights_output_widget2"), + ui.tags.hr(), + ui.h3("Filtered Flights Table"), + ui.output_text("flights_record_count_string"), + ui.output_table("flights_filtered_table"), + ui.tags.hr(), + ) + ) diff --git a/mtcars.csv b/mtcars.csv new file mode 100644 index 0000000..2abcf28 --- /dev/null +++ b/mtcars.csv @@ -0,0 +1,33 @@ +"mpg","cyl","disp","hp","drat","wt","qsec","vs","am","gear","carb" +21,6,160,110,3.9,2.62,16.46,0,1,4,4 +21,6,160,110,3.9,2.875,17.02,0,1,4,4 +22.8,4,108,93,3.85,2.32,18.61,1,1,4,1 +21.4,6,258,110,3.08,3.215,19.44,1,0,3,1 +18.7,8,360,175,3.15,3.44,17.02,0,0,3,2 +18.1,6,225,105,2.76,3.46,20.22,1,0,3,1 +14.3,8,360,245,3.21,3.57,15.84,0,0,3,4 +24.4,4,146.7,62,3.69,3.19,20,1,0,4,2 +22.8,4,140.8,95,3.92,3.15,22.9,1,0,4,2 +19.2,6,167.6,123,3.92,3.44,18.3,1,0,4,4 +17.8,6,167.6,123,3.92,3.44,18.9,1,0,4,4 +16.4,8,275.8,180,3.07,4.07,17.4,0,0,3,3 +17.3,8,275.8,180,3.07,3.73,17.6,0,0,3,3 +15.2,8,275.8,180,3.07,3.78,18,0,0,3,3 +10.4,8,472,205,2.93,5.25,17.98,0,0,3,4 +10.4,8,460,215,3,5.424,17.82,0,0,3,4 +14.7,8,440,230,3.23,5.345,17.42,0,0,3,4 +32.4,4,78.7,66,4.08,2.2,19.47,1,1,4,1 +30.4,4,75.7,52,4.93,1.615,18.52,1,1,4,2 +33.9,4,71.1,65,4.22,1.835,19.9,1,1,4,1 +21.5,4,120.1,97,3.7,2.465,20.01,1,0,3,1 +15.5,8,318,150,2.76,3.52,16.87,0,0,3,2 +15.2,8,304,150,3.15,3.435,17.3,0,0,3,2 +13.3,8,350,245,3.73,3.84,15.41,0,0,3,4 +19.2,8,400,175,3.08,3.845,17.05,0,0,3,2 +27.3,4,79,66,4.08,1.935,18.9,1,1,4,1 +26,4,120.3,91,4.43,2.14,16.7,0,1,5,2 +30.4,4,95.1,113,3.77,1.513,16.9,1,1,5,2 +15.8,8,351,264,4.22,3.17,14.5,0,1,5,4 +19.7,6,145,175,3.62,2.77,15.5,0,1,5,6 +15,8,301,335,3.54,3.57,14.6,0,1,5,8 +21.4,4,121,109,4.11,2.78,18.6,1,1,4,2 diff --git a/mtcars_server.py b/mtcars_server.py new file mode 100644 index 0000000..f349cc1 --- /dev/null +++ b/mtcars_server.py @@ -0,0 +1,107 @@ +''' +Purpose: Provide reactive output for the MT Cars dataset. + +Use inputs from the UI Sidebar to filter the dataset. + +Update ouputs in the UI Main Panel. + +Matching the IDs in the UI Sidebar and function/ouput names in the UI Main Panel +to this server code is critical. They are case sensitive and must match exactly. + +''' + +from shiny import * +import matplotlib.pyplot as plt +import pandas as pd +from bokeh.plotting import figure +from plotnine import aes, geom_point, ggplot, ggtitle +from shinywidgets import render_widget +import plotly.express as px +import plotly.graph_objs as go + +from util_logger import setup_logger +logger, logname = setup_logger(__name__) + +def get_mtcars_server_functions(input, output, session): + + original_df = pd.read_csv("mtcars.csv") + total_count = len(original_df) + + reactive_df = reactive.Value() + + @reactive.Effect + @reactive.event(input.MTCARS_MPG_RANGE) + def _(): + + df = original_df.copy() + + input_range = input.MTCARS_MPG_RANGE() + input_min = input_range[0] + input_max = input_range[1] + + ''' + Filter the dataframe to just those greater than or equal to the min + and less than or equal to the max + Note: The ampersand (&) is the Python operator for AND + The column name is in quotes and is "mpg". + You must be familiar with the dataset to know the column names. + ''' + + filtered_df = df[(df["mpg"] >= input_min) & (df["mpg"] <= input_max)] + + # Set the reactive value + reactive_df.set(filtered_df) + + + @output + @render.text + def mtcars_record_count_string(): + filtered_df = reactive_df.get() + filtered_count = len(filtered_df) + message = f"Showing {filtered_count} of {total_count} records" + logger.debug(f"filter message: {message}") + return message + + @output + @render.table + def mtcars_filtered_table(): + filtered_df = reactive_df.get() + return filtered_df + + @output + @render_widget + def mtcars_output_widget1(): + df = reactive_df.get() + plotly_express_plot = px.scatter(df, x="mpg", y="hp", color="cyl", size="wt") + plotly_express_plot.update_layout(title="MT Cars with Plotly Express") + return plotly_express_plot + + @output + @render.plot + def mtcars_plot1(): + df = reactive_df.get() + matplotlib_fig, ax = plt.subplots() + plt.title("MT Cars with matplotlib") + ax.scatter(df["wt"], df["mpg"]) + return matplotlib_fig + + @output + @render.plot + def mtcars_plot2(): + df = reactive_df.get() + plotnine_plot = ( + ggplot(df, aes("wt", "mpg")) + + geom_point() + + ggtitle("MT Cars with plotnine") + ) + + return plotnine_plot + + # return a list of function names for use in reactive outputs + return [ + mtcars_record_count_string, + mtcars_filtered_table, + mtcars_output_widget1, + mtcars_plot1, + mtcars_plot2, + ] diff --git a/mtcars_ui_inputs.py b/mtcars_ui_inputs.py new file mode 100644 index 0000000..fd7074d --- /dev/null +++ b/mtcars_ui_inputs.py @@ -0,0 +1,46 @@ +""" +Purpose: Provide user interaction options for the MT Cars dataset. + +IDs must be unique. They are capitalized in this app for clarity (not typical). +The IDs are case-sensitive and must match the server code exactly. + +We prefaced IDs with the dataset name to avoid naming conflicts. + +""" +from shiny import ui + + +def get_mtcars_sidebar(): + return ui.panel_sidebar( + ui.h2("MT Cars Interaction"), + ui.tags.hr(), + ui.input_slider( + "MTCARS_MPG_RANGE", + "Miles Per Gallon (MPG)", + min=10, + max=35, + value=[10, 35], + ), + ui.tags.hr(), + ui.tags.section( + ui.h3("MT Cars Table"), + ui.tags.p("Description of each field in the table:"), + ui.tags.ul( + ui.tags.li("mpg: Miles per Gallon"), + ui.tags.li("cyl: Number of cylinders"), + ui.tags.li("disp: Displacement (cubic inches)"), + ui.tags.li("hp: Gross horsepower"), + ui.tags.li("drat: Rear axle ratio"), + ui.tags.li("wt: Weight (1,000 lbs)"), + ui.tags.li("qsec: 1/4 mile time"), + ui.tags.li("vs: V/S (Engine shape; 0 = V-shaped, 1 = Straight)"), + ui.tags.li("am: Transmission (0 = Automatic, 1 = Manual)"), + ui.tags.li("gear: Number of forward gears"), + ui.tags.li("carb: Number of carburetors"), + ), + ui.output_table("cars_table"), + ), + ui.tags.hr(), + ui.p("Please be patient. The charts may take a few seconds to load."), + ui.tags.hr(), + ) diff --git a/mtcars_ui_outputs.py b/mtcars_ui_outputs.py new file mode 100644 index 0000000..95997a9 --- /dev/null +++ b/mtcars_ui_outputs.py @@ -0,0 +1,23 @@ +""" +Purpose: Display ouput for the MT Cars dataset. +""" +from shiny import ui +from shinywidgets import output_widget + + +def get_mtcars_main(): + return ui.panel_main( + ui.h2("Main Panel with Reactive Output"), + ui.tags.hr(), + ui.tags.section( + ui.h3("Filtered Cars: Charts"), + output_widget("mtcars_output_widget1"), + ui.output_plot("mtcars_plot1"), + ui.output_plot("mtcars_plot2"), + ui.tags.hr(), + ui.h3("Filtered MT Cars Table"), + ui.output_text("mtcars_record_count_string"), + ui.output_table("mtcars_filtered_table"), + ui.tags.hr(), + ), + ) diff --git a/penguins.csv b/penguins.csv new file mode 100644 index 0000000..54d30b2 --- /dev/null +++ b/penguins.csv @@ -0,0 +1,345 @@ +,species,island,bill_length_mm,bill_depth_mm,flipper_length_mm,body_mass_g,sex +0,Adelie,Torgersen,39.1,18.7,181.0,3750.0,Male +1,Adelie,Torgersen,39.5,17.4,186.0,3800.0,Female +2,Adelie,Torgersen,40.3,18.0,195.0,3250.0,Female +3,Adelie,Torgersen,,,,, +4,Adelie,Torgersen,36.7,19.3,193.0,3450.0,Female +5,Adelie,Torgersen,39.3,20.6,190.0,3650.0,Male +6,Adelie,Torgersen,38.9,17.8,181.0,3625.0,Female +7,Adelie,Torgersen,39.2,19.6,195.0,4675.0,Male +8,Adelie,Torgersen,34.1,18.1,193.0,3475.0, +9,Adelie,Torgersen,42.0,20.2,190.0,4250.0, +10,Adelie,Torgersen,37.8,17.1,186.0,3300.0, +11,Adelie,Torgersen,37.8,17.3,180.0,3700.0, +12,Adelie,Torgersen,41.1,17.6,182.0,3200.0,Female +13,Adelie,Torgersen,38.6,21.2,191.0,3800.0,Male +14,Adelie,Torgersen,34.6,21.1,198.0,4400.0,Male +15,Adelie,Torgersen,36.6,17.8,185.0,3700.0,Female +16,Adelie,Torgersen,38.7,19.0,195.0,3450.0,Female +17,Adelie,Torgersen,42.5,20.7,197.0,4500.0,Male +18,Adelie,Torgersen,34.4,18.4,184.0,3325.0,Female +19,Adelie,Torgersen,46.0,21.5,194.0,4200.0,Male +20,Adelie,Biscoe,37.8,18.3,174.0,3400.0,Female +21,Adelie,Biscoe,37.7,18.7,180.0,3600.0,Male +22,Adelie,Biscoe,35.9,19.2,189.0,3800.0,Female +23,Adelie,Biscoe,38.2,18.1,185.0,3950.0,Male +24,Adelie,Biscoe,38.8,17.2,180.0,3800.0,Male +25,Adelie,Biscoe,35.3,18.9,187.0,3800.0,Female +26,Adelie,Biscoe,40.6,18.6,183.0,3550.0,Male +27,Adelie,Biscoe,40.5,17.9,187.0,3200.0,Female +28,Adelie,Biscoe,37.9,18.6,172.0,3150.0,Female +29,Adelie,Biscoe,40.5,18.9,180.0,3950.0,Male +30,Adelie,Dream,39.5,16.7,178.0,3250.0,Female +31,Adelie,Dream,37.2,18.1,178.0,3900.0,Male +32,Adelie,Dream,39.5,17.8,188.0,3300.0,Female +33,Adelie,Dream,40.9,18.9,184.0,3900.0,Male +34,Adelie,Dream,36.4,17.0,195.0,3325.0,Female +35,Adelie,Dream,39.2,21.1,196.0,4150.0,Male +36,Adelie,Dream,38.8,20.0,190.0,3950.0,Male +37,Adelie,Dream,42.2,18.5,180.0,3550.0,Female +38,Adelie,Dream,37.6,19.3,181.0,3300.0,Female +39,Adelie,Dream,39.8,19.1,184.0,4650.0,Male +40,Adelie,Dream,36.5,18.0,182.0,3150.0,Female +41,Adelie,Dream,40.8,18.4,195.0,3900.0,Male +42,Adelie,Dream,36.0,18.5,186.0,3100.0,Female +43,Adelie,Dream,44.1,19.7,196.0,4400.0,Male +44,Adelie,Dream,37.0,16.9,185.0,3000.0,Female +45,Adelie,Dream,39.6,18.8,190.0,4600.0,Male +46,Adelie,Dream,41.1,19.0,182.0,3425.0,Male +47,Adelie,Dream,37.5,18.9,179.0,2975.0, +48,Adelie,Dream,36.0,17.9,190.0,3450.0,Female +49,Adelie,Dream,42.3,21.2,191.0,4150.0,Male +50,Adelie,Biscoe,39.6,17.7,186.0,3500.0,Female +51,Adelie,Biscoe,40.1,18.9,188.0,4300.0,Male +52,Adelie,Biscoe,35.0,17.9,190.0,3450.0,Female +53,Adelie,Biscoe,42.0,19.5,200.0,4050.0,Male +54,Adelie,Biscoe,34.5,18.1,187.0,2900.0,Female +55,Adelie,Biscoe,41.4,18.6,191.0,3700.0,Male +56,Adelie,Biscoe,39.0,17.5,186.0,3550.0,Female +57,Adelie,Biscoe,40.6,18.8,193.0,3800.0,Male +58,Adelie,Biscoe,36.5,16.6,181.0,2850.0,Female +59,Adelie,Biscoe,37.6,19.1,194.0,3750.0,Male +60,Adelie,Biscoe,35.7,16.9,185.0,3150.0,Female +61,Adelie,Biscoe,41.3,21.1,195.0,4400.0,Male +62,Adelie,Biscoe,37.6,17.0,185.0,3600.0,Female +63,Adelie,Biscoe,41.1,18.2,192.0,4050.0,Male +64,Adelie,Biscoe,36.4,17.1,184.0,2850.0,Female +65,Adelie,Biscoe,41.6,18.0,192.0,3950.0,Male +66,Adelie,Biscoe,35.5,16.2,195.0,3350.0,Female +67,Adelie,Biscoe,41.1,19.1,188.0,4100.0,Male +68,Adelie,Torgersen,35.9,16.6,190.0,3050.0,Female +69,Adelie,Torgersen,41.8,19.4,198.0,4450.0,Male +70,Adelie,Torgersen,33.5,19.0,190.0,3600.0,Female +71,Adelie,Torgersen,39.7,18.4,190.0,3900.0,Male +72,Adelie,Torgersen,39.6,17.2,196.0,3550.0,Female +73,Adelie,Torgersen,45.8,18.9,197.0,4150.0,Male +74,Adelie,Torgersen,35.5,17.5,190.0,3700.0,Female +75,Adelie,Torgersen,42.8,18.5,195.0,4250.0,Male +76,Adelie,Torgersen,40.9,16.8,191.0,3700.0,Female +77,Adelie,Torgersen,37.2,19.4,184.0,3900.0,Male +78,Adelie,Torgersen,36.2,16.1,187.0,3550.0,Female +79,Adelie,Torgersen,42.1,19.1,195.0,4000.0,Male +80,Adelie,Torgersen,34.6,17.2,189.0,3200.0,Female +81,Adelie,Torgersen,42.9,17.6,196.0,4700.0,Male +82,Adelie,Torgersen,36.7,18.8,187.0,3800.0,Female +83,Adelie,Torgersen,35.1,19.4,193.0,4200.0,Male +84,Adelie,Dream,37.3,17.8,191.0,3350.0,Female +85,Adelie,Dream,41.3,20.3,194.0,3550.0,Male +86,Adelie,Dream,36.3,19.5,190.0,3800.0,Male +87,Adelie,Dream,36.9,18.6,189.0,3500.0,Female +88,Adelie,Dream,38.3,19.2,189.0,3950.0,Male +89,Adelie,Dream,38.9,18.8,190.0,3600.0,Female +90,Adelie,Dream,35.7,18.0,202.0,3550.0,Female +91,Adelie,Dream,41.1,18.1,205.0,4300.0,Male +92,Adelie,Dream,34.0,17.1,185.0,3400.0,Female +93,Adelie,Dream,39.6,18.1,186.0,4450.0,Male +94,Adelie,Dream,36.2,17.3,187.0,3300.0,Female +95,Adelie,Dream,40.8,18.9,208.0,4300.0,Male +96,Adelie,Dream,38.1,18.6,190.0,3700.0,Female +97,Adelie,Dream,40.3,18.5,196.0,4350.0,Male +98,Adelie,Dream,33.1,16.1,178.0,2900.0,Female +99,Adelie,Dream,43.2,18.5,192.0,4100.0,Male +100,Adelie,Biscoe,35.0,17.9,192.0,3725.0,Female +101,Adelie,Biscoe,41.0,20.0,203.0,4725.0,Male +102,Adelie,Biscoe,37.7,16.0,183.0,3075.0,Female +103,Adelie,Biscoe,37.8,20.0,190.0,4250.0,Male +104,Adelie,Biscoe,37.9,18.6,193.0,2925.0,Female +105,Adelie,Biscoe,39.7,18.9,184.0,3550.0,Male +106,Adelie,Biscoe,38.6,17.2,199.0,3750.0,Female +107,Adelie,Biscoe,38.2,20.0,190.0,3900.0,Male +108,Adelie,Biscoe,38.1,17.0,181.0,3175.0,Female +109,Adelie,Biscoe,43.2,19.0,197.0,4775.0,Male +110,Adelie,Biscoe,38.1,16.5,198.0,3825.0,Female +111,Adelie,Biscoe,45.6,20.3,191.0,4600.0,Male +112,Adelie,Biscoe,39.7,17.7,193.0,3200.0,Female +113,Adelie,Biscoe,42.2,19.5,197.0,4275.0,Male +114,Adelie,Biscoe,39.6,20.7,191.0,3900.0,Female +115,Adelie,Biscoe,42.7,18.3,196.0,4075.0,Male +116,Adelie,Torgersen,38.6,17.0,188.0,2900.0,Female +117,Adelie,Torgersen,37.3,20.5,199.0,3775.0,Male +118,Adelie,Torgersen,35.7,17.0,189.0,3350.0,Female +119,Adelie,Torgersen,41.1,18.6,189.0,3325.0,Male +120,Adelie,Torgersen,36.2,17.2,187.0,3150.0,Female +121,Adelie,Torgersen,37.7,19.8,198.0,3500.0,Male +122,Adelie,Torgersen,40.2,17.0,176.0,3450.0,Female +123,Adelie,Torgersen,41.4,18.5,202.0,3875.0,Male +124,Adelie,Torgersen,35.2,15.9,186.0,3050.0,Female +125,Adelie,Torgersen,40.6,19.0,199.0,4000.0,Male +126,Adelie,Torgersen,38.8,17.6,191.0,3275.0,Female +127,Adelie,Torgersen,41.5,18.3,195.0,4300.0,Male +128,Adelie,Torgersen,39.0,17.1,191.0,3050.0,Female +129,Adelie,Torgersen,44.1,18.0,210.0,4000.0,Male +130,Adelie,Torgersen,38.5,17.9,190.0,3325.0,Female +131,Adelie,Torgersen,43.1,19.2,197.0,3500.0,Male +132,Adelie,Dream,36.8,18.5,193.0,3500.0,Female +133,Adelie,Dream,37.5,18.5,199.0,4475.0,Male +134,Adelie,Dream,38.1,17.6,187.0,3425.0,Female +135,Adelie,Dream,41.1,17.5,190.0,3900.0,Male +136,Adelie,Dream,35.6,17.5,191.0,3175.0,Female +137,Adelie,Dream,40.2,20.1,200.0,3975.0,Male +138,Adelie,Dream,37.0,16.5,185.0,3400.0,Female +139,Adelie,Dream,39.7,17.9,193.0,4250.0,Male +140,Adelie,Dream,40.2,17.1,193.0,3400.0,Female +141,Adelie,Dream,40.6,17.2,187.0,3475.0,Male +142,Adelie,Dream,32.1,15.5,188.0,3050.0,Female +143,Adelie,Dream,40.7,17.0,190.0,3725.0,Male +144,Adelie,Dream,37.3,16.8,192.0,3000.0,Female +145,Adelie,Dream,39.0,18.7,185.0,3650.0,Male +146,Adelie,Dream,39.2,18.6,190.0,4250.0,Male +147,Adelie,Dream,36.6,18.4,184.0,3475.0,Female +148,Adelie,Dream,36.0,17.8,195.0,3450.0,Female +149,Adelie,Dream,37.8,18.1,193.0,3750.0,Male +150,Adelie,Dream,36.0,17.1,187.0,3700.0,Female +151,Adelie,Dream,41.5,18.5,201.0,4000.0,Male +152,Chinstrap,Dream,46.5,17.9,192.0,3500.0,Female +153,Chinstrap,Dream,50.0,19.5,196.0,3900.0,Male +154,Chinstrap,Dream,51.3,19.2,193.0,3650.0,Male +155,Chinstrap,Dream,45.4,18.7,188.0,3525.0,Female +156,Chinstrap,Dream,52.7,19.8,197.0,3725.0,Male +157,Chinstrap,Dream,45.2,17.8,198.0,3950.0,Female +158,Chinstrap,Dream,46.1,18.2,178.0,3250.0,Female +159,Chinstrap,Dream,51.3,18.2,197.0,3750.0,Male +160,Chinstrap,Dream,46.0,18.9,195.0,4150.0,Female +161,Chinstrap,Dream,51.3,19.9,198.0,3700.0,Male +162,Chinstrap,Dream,46.6,17.8,193.0,3800.0,Female +163,Chinstrap,Dream,51.7,20.3,194.0,3775.0,Male +164,Chinstrap,Dream,47.0,17.3,185.0,3700.0,Female +165,Chinstrap,Dream,52.0,18.1,201.0,4050.0,Male +166,Chinstrap,Dream,45.9,17.1,190.0,3575.0,Female +167,Chinstrap,Dream,50.5,19.6,201.0,4050.0,Male +168,Chinstrap,Dream,50.3,20.0,197.0,3300.0,Male +169,Chinstrap,Dream,58.0,17.8,181.0,3700.0,Female +170,Chinstrap,Dream,46.4,18.6,190.0,3450.0,Female +171,Chinstrap,Dream,49.2,18.2,195.0,4400.0,Male +172,Chinstrap,Dream,42.4,17.3,181.0,3600.0,Female +173,Chinstrap,Dream,48.5,17.5,191.0,3400.0,Male +174,Chinstrap,Dream,43.2,16.6,187.0,2900.0,Female +175,Chinstrap,Dream,50.6,19.4,193.0,3800.0,Male +176,Chinstrap,Dream,46.7,17.9,195.0,3300.0,Female +177,Chinstrap,Dream,52.0,19.0,197.0,4150.0,Male +178,Chinstrap,Dream,50.5,18.4,200.0,3400.0,Female +179,Chinstrap,Dream,49.5,19.0,200.0,3800.0,Male +180,Chinstrap,Dream,46.4,17.8,191.0,3700.0,Female +181,Chinstrap,Dream,52.8,20.0,205.0,4550.0,Male +182,Chinstrap,Dream,40.9,16.6,187.0,3200.0,Female +183,Chinstrap,Dream,54.2,20.8,201.0,4300.0,Male +184,Chinstrap,Dream,42.5,16.7,187.0,3350.0,Female +185,Chinstrap,Dream,51.0,18.8,203.0,4100.0,Male +186,Chinstrap,Dream,49.7,18.6,195.0,3600.0,Male +187,Chinstrap,Dream,47.5,16.8,199.0,3900.0,Female +188,Chinstrap,Dream,47.6,18.3,195.0,3850.0,Female +189,Chinstrap,Dream,52.0,20.7,210.0,4800.0,Male +190,Chinstrap,Dream,46.9,16.6,192.0,2700.0,Female +191,Chinstrap,Dream,53.5,19.9,205.0,4500.0,Male +192,Chinstrap,Dream,49.0,19.5,210.0,3950.0,Male +193,Chinstrap,Dream,46.2,17.5,187.0,3650.0,Female +194,Chinstrap,Dream,50.9,19.1,196.0,3550.0,Male +195,Chinstrap,Dream,45.5,17.0,196.0,3500.0,Female +196,Chinstrap,Dream,50.9,17.9,196.0,3675.0,Female +197,Chinstrap,Dream,50.8,18.5,201.0,4450.0,Male +198,Chinstrap,Dream,50.1,17.9,190.0,3400.0,Female +199,Chinstrap,Dream,49.0,19.6,212.0,4300.0,Male +200,Chinstrap,Dream,51.5,18.7,187.0,3250.0,Male +201,Chinstrap,Dream,49.8,17.3,198.0,3675.0,Female +202,Chinstrap,Dream,48.1,16.4,199.0,3325.0,Female +203,Chinstrap,Dream,51.4,19.0,201.0,3950.0,Male +204,Chinstrap,Dream,45.7,17.3,193.0,3600.0,Female +205,Chinstrap,Dream,50.7,19.7,203.0,4050.0,Male +206,Chinstrap,Dream,42.5,17.3,187.0,3350.0,Female +207,Chinstrap,Dream,52.2,18.8,197.0,3450.0,Male +208,Chinstrap,Dream,45.2,16.6,191.0,3250.0,Female +209,Chinstrap,Dream,49.3,19.9,203.0,4050.0,Male +210,Chinstrap,Dream,50.2,18.8,202.0,3800.0,Male +211,Chinstrap,Dream,45.6,19.4,194.0,3525.0,Female +212,Chinstrap,Dream,51.9,19.5,206.0,3950.0,Male +213,Chinstrap,Dream,46.8,16.5,189.0,3650.0,Female +214,Chinstrap,Dream,45.7,17.0,195.0,3650.0,Female +215,Chinstrap,Dream,55.8,19.8,207.0,4000.0,Male +216,Chinstrap,Dream,43.5,18.1,202.0,3400.0,Female +217,Chinstrap,Dream,49.6,18.2,193.0,3775.0,Male +218,Chinstrap,Dream,50.8,19.0,210.0,4100.0,Male +219,Chinstrap,Dream,50.2,18.7,198.0,3775.0,Female +220,Gentoo,Biscoe,46.1,13.2,211.0,4500.0,Female +221,Gentoo,Biscoe,50.0,16.3,230.0,5700.0,Male +222,Gentoo,Biscoe,48.7,14.1,210.0,4450.0,Female +223,Gentoo,Biscoe,50.0,15.2,218.0,5700.0,Male +224,Gentoo,Biscoe,47.6,14.5,215.0,5400.0,Male +225,Gentoo,Biscoe,46.5,13.5,210.0,4550.0,Female +226,Gentoo,Biscoe,45.4,14.6,211.0,4800.0,Female +227,Gentoo,Biscoe,46.7,15.3,219.0,5200.0,Male +228,Gentoo,Biscoe,43.3,13.4,209.0,4400.0,Female +229,Gentoo,Biscoe,46.8,15.4,215.0,5150.0,Male +230,Gentoo,Biscoe,40.9,13.7,214.0,4650.0,Female +231,Gentoo,Biscoe,49.0,16.1,216.0,5550.0,Male +232,Gentoo,Biscoe,45.5,13.7,214.0,4650.0,Female +233,Gentoo,Biscoe,48.4,14.6,213.0,5850.0,Male +234,Gentoo,Biscoe,45.8,14.6,210.0,4200.0,Female +235,Gentoo,Biscoe,49.3,15.7,217.0,5850.0,Male +236,Gentoo,Biscoe,42.0,13.5,210.0,4150.0,Female +237,Gentoo,Biscoe,49.2,15.2,221.0,6300.0,Male +238,Gentoo,Biscoe,46.2,14.5,209.0,4800.0,Female +239,Gentoo,Biscoe,48.7,15.1,222.0,5350.0,Male +240,Gentoo,Biscoe,50.2,14.3,218.0,5700.0,Male +241,Gentoo,Biscoe,45.1,14.5,215.0,5000.0,Female +242,Gentoo,Biscoe,46.5,14.5,213.0,4400.0,Female +243,Gentoo,Biscoe,46.3,15.8,215.0,5050.0,Male +244,Gentoo,Biscoe,42.9,13.1,215.0,5000.0,Female +245,Gentoo,Biscoe,46.1,15.1,215.0,5100.0,Male +246,Gentoo,Biscoe,44.5,14.3,216.0,4100.0, +247,Gentoo,Biscoe,47.8,15.0,215.0,5650.0,Male +248,Gentoo,Biscoe,48.2,14.3,210.0,4600.0,Female +249,Gentoo,Biscoe,50.0,15.3,220.0,5550.0,Male +250,Gentoo,Biscoe,47.3,15.3,222.0,5250.0,Male +251,Gentoo,Biscoe,42.8,14.2,209.0,4700.0,Female +252,Gentoo,Biscoe,45.1,14.5,207.0,5050.0,Female +253,Gentoo,Biscoe,59.6,17.0,230.0,6050.0,Male +254,Gentoo,Biscoe,49.1,14.8,220.0,5150.0,Female +255,Gentoo,Biscoe,48.4,16.3,220.0,5400.0,Male +256,Gentoo,Biscoe,42.6,13.7,213.0,4950.0,Female +257,Gentoo,Biscoe,44.4,17.3,219.0,5250.0,Male +258,Gentoo,Biscoe,44.0,13.6,208.0,4350.0,Female +259,Gentoo,Biscoe,48.7,15.7,208.0,5350.0,Male +260,Gentoo,Biscoe,42.7,13.7,208.0,3950.0,Female +261,Gentoo,Biscoe,49.6,16.0,225.0,5700.0,Male +262,Gentoo,Biscoe,45.3,13.7,210.0,4300.0,Female +263,Gentoo,Biscoe,49.6,15.0,216.0,4750.0,Male +264,Gentoo,Biscoe,50.5,15.9,222.0,5550.0,Male +265,Gentoo,Biscoe,43.6,13.9,217.0,4900.0,Female +266,Gentoo,Biscoe,45.5,13.9,210.0,4200.0,Female +267,Gentoo,Biscoe,50.5,15.9,225.0,5400.0,Male +268,Gentoo,Biscoe,44.9,13.3,213.0,5100.0,Female +269,Gentoo,Biscoe,45.2,15.8,215.0,5300.0,Male +270,Gentoo,Biscoe,46.6,14.2,210.0,4850.0,Female +271,Gentoo,Biscoe,48.5,14.1,220.0,5300.0,Male +272,Gentoo,Biscoe,45.1,14.4,210.0,4400.0,Female +273,Gentoo,Biscoe,50.1,15.0,225.0,5000.0,Male +274,Gentoo,Biscoe,46.5,14.4,217.0,4900.0,Female +275,Gentoo,Biscoe,45.0,15.4,220.0,5050.0,Male +276,Gentoo,Biscoe,43.8,13.9,208.0,4300.0,Female +277,Gentoo,Biscoe,45.5,15.0,220.0,5000.0,Male +278,Gentoo,Biscoe,43.2,14.5,208.0,4450.0,Female +279,Gentoo,Biscoe,50.4,15.3,224.0,5550.0,Male +280,Gentoo,Biscoe,45.3,13.8,208.0,4200.0,Female +281,Gentoo,Biscoe,46.2,14.9,221.0,5300.0,Male +282,Gentoo,Biscoe,45.7,13.9,214.0,4400.0,Female +283,Gentoo,Biscoe,54.3,15.7,231.0,5650.0,Male +284,Gentoo,Biscoe,45.8,14.2,219.0,4700.0,Female +285,Gentoo,Biscoe,49.8,16.8,230.0,5700.0,Male +286,Gentoo,Biscoe,46.2,14.4,214.0,4650.0, +287,Gentoo,Biscoe,49.5,16.2,229.0,5800.0,Male +288,Gentoo,Biscoe,43.5,14.2,220.0,4700.0,Female +289,Gentoo,Biscoe,50.7,15.0,223.0,5550.0,Male +290,Gentoo,Biscoe,47.7,15.0,216.0,4750.0,Female +291,Gentoo,Biscoe,46.4,15.6,221.0,5000.0,Male +292,Gentoo,Biscoe,48.2,15.6,221.0,5100.0,Male +293,Gentoo,Biscoe,46.5,14.8,217.0,5200.0,Female +294,Gentoo,Biscoe,46.4,15.0,216.0,4700.0,Female +295,Gentoo,Biscoe,48.6,16.0,230.0,5800.0,Male +296,Gentoo,Biscoe,47.5,14.2,209.0,4600.0,Female +297,Gentoo,Biscoe,51.1,16.3,220.0,6000.0,Male +298,Gentoo,Biscoe,45.2,13.8,215.0,4750.0,Female +299,Gentoo,Biscoe,45.2,16.4,223.0,5950.0,Male +300,Gentoo,Biscoe,49.1,14.5,212.0,4625.0,Female +301,Gentoo,Biscoe,52.5,15.6,221.0,5450.0,Male +302,Gentoo,Biscoe,47.4,14.6,212.0,4725.0,Female +303,Gentoo,Biscoe,50.0,15.9,224.0,5350.0,Male +304,Gentoo,Biscoe,44.9,13.8,212.0,4750.0,Female +305,Gentoo,Biscoe,50.8,17.3,228.0,5600.0,Male +306,Gentoo,Biscoe,43.4,14.4,218.0,4600.0,Female +307,Gentoo,Biscoe,51.3,14.2,218.0,5300.0,Male +308,Gentoo,Biscoe,47.5,14.0,212.0,4875.0,Female +309,Gentoo,Biscoe,52.1,17.0,230.0,5550.0,Male +310,Gentoo,Biscoe,47.5,15.0,218.0,4950.0,Female +311,Gentoo,Biscoe,52.2,17.1,228.0,5400.0,Male +312,Gentoo,Biscoe,45.5,14.5,212.0,4750.0,Female +313,Gentoo,Biscoe,49.5,16.1,224.0,5650.0,Male +314,Gentoo,Biscoe,44.5,14.7,214.0,4850.0,Female +315,Gentoo,Biscoe,50.8,15.7,226.0,5200.0,Male +316,Gentoo,Biscoe,49.4,15.8,216.0,4925.0,Male +317,Gentoo,Biscoe,46.9,14.6,222.0,4875.0,Female +318,Gentoo,Biscoe,48.4,14.4,203.0,4625.0,Female +319,Gentoo,Biscoe,51.1,16.5,225.0,5250.0,Male +320,Gentoo,Biscoe,48.5,15.0,219.0,4850.0,Female +321,Gentoo,Biscoe,55.9,17.0,228.0,5600.0,Male +322,Gentoo,Biscoe,47.2,15.5,215.0,4975.0,Female +323,Gentoo,Biscoe,49.1,15.0,228.0,5500.0,Male +324,Gentoo,Biscoe,47.3,13.8,216.0,4725.0, +325,Gentoo,Biscoe,46.8,16.1,215.0,5500.0,Male +326,Gentoo,Biscoe,41.7,14.7,210.0,4700.0,Female +327,Gentoo,Biscoe,53.4,15.8,219.0,5500.0,Male +328,Gentoo,Biscoe,43.3,14.0,208.0,4575.0,Female +329,Gentoo,Biscoe,48.1,15.1,209.0,5500.0,Male +330,Gentoo,Biscoe,50.5,15.2,216.0,5000.0,Female +331,Gentoo,Biscoe,49.8,15.9,229.0,5950.0,Male +332,Gentoo,Biscoe,43.5,15.2,213.0,4650.0,Female +333,Gentoo,Biscoe,51.5,16.3,230.0,5500.0,Male +334,Gentoo,Biscoe,46.2,14.1,217.0,4375.0,Female +335,Gentoo,Biscoe,55.1,16.0,230.0,5850.0,Male +336,Gentoo,Biscoe,44.5,15.7,217.0,4875.0, +337,Gentoo,Biscoe,48.8,16.2,222.0,6000.0,Male +338,Gentoo,Biscoe,47.2,13.7,214.0,4925.0,Female +339,Gentoo,Biscoe,,,,, +340,Gentoo,Biscoe,46.8,14.3,215.0,4850.0,Female +341,Gentoo,Biscoe,50.4,15.7,222.0,5750.0,Male +342,Gentoo,Biscoe,45.2,14.8,212.0,5200.0,Female +343,Gentoo,Biscoe,49.9,16.1,213.0,5400.0,Male diff --git a/penguins.xlsx b/penguins.xlsx new file mode 100644 index 0000000..dec9dde Binary files /dev/null and b/penguins.xlsx differ diff --git a/penguins_server.py b/penguins_server.py new file mode 100644 index 0000000..62449e2 --- /dev/null +++ b/penguins_server.py @@ -0,0 +1,125 @@ +''' +Purpose: Provide reactive output for the Penguins dataset. + +Use inputs from the UI Sidebar to filter the dataset. + +Update ouputs in the UI Main Panel. + +Matching the IDs in the UI Sidebar and function/ouput names in the UI Main Panel +to this server code is critical. They are case sensitive and must match exactly. + +''' +from shiny import * +import pandas as pd +from bokeh.plotting import figure +from shinywidgets import render_widget +import plotly.express as px +import plotly.graph_objs as go +from shinywidgets import render_widget +import jupyter_bokeh as jbk + +from util_logger import setup_logger +logger, logname = setup_logger(__name__) + +def get_penguins_server_functions(input, output, session): + + + # Local variables just for this function + # The original dataset and the total record count + + original_df = pd.read_excel("penguins.xlsx") + total_count = len(original_df) + + # Create a reactive value to hold the filtered pandas dataframe + reactive_df = reactive.Value() + + # Create a reactive effect to set the reactive value when inputs change + # List all the inputs that should trigger this update + + @reactive.Effect + @reactive.event(input.PENGUIN_BODY_MASS_RANGE, input.PENGUIN_MAX_BILL, + input.PENGUIN_SPECIES_Adelie, input.PENGUIN_SPECIES_Chinstrap, + input.PENGUIN_SPECIES_Gentoo, input.PENGUIN_GENDER ) + def _(): + ''' Reactive effect to update the filtered dataframe when inputs change. + This is the only way to set a reactive value (after initialization). + It doesn't need a name, because no one calls it directly.''' + + logger.info("UI inputs changed. Updating penguins reactive df") + + df = original_df.copy() + + # Body mass is a range + input_range = input.PENGUIN_BODY_MASS_RANGE() + input_min = input_range[0] + input_max = input_range[1] + body_mass_filter = (df["body_mass_g"] >= input_min) & (df["body_mass_g"] <= input_max) + df = df[body_mass_filter] + + # Bill length is a max number + bill_length_filter = df["bill_length_mm"] <= input.PENGUIN_MAX_BILL() + df = df[bill_length_filter] + + # Species is a list of checkboxes (a list of possible values) + show_species_list = [] + if input.PENGUIN_SPECIES_Adelie(): + show_species_list.append("Adelie") + if input.PENGUIN_SPECIES_Chinstrap(): + show_species_list.append("Chinstrap") + if input.PENGUIN_SPECIES_Gentoo(): + show_species_list.append("Gentoo") + show_species_list = show_species_list or ["Adelie", "Chinstrap", "Gentoo"] + species_filter = df["species"].isin(show_species_list) + df = df[species_filter] + + # Gender is a radio button + input_gender = input.PENGUIN_GENDER() + gender_dict = {"a": "All", "f": "Female", "m": "Male"} + if input_gender != "a": + gender_filter = df["sex"] == gender_dict[input_gender] + df = df[gender_filter] + + #logger.debug(f"filtered penguins df: {df}") + reactive_df.set(df) + + + @output + @render.text + def penguins_record_count_string(): + logger.debug("Triggered: penguins_filter_record_count_string") + filtered_count = len(reactive_df.get()) + message = f"Showing {filtered_count} of {total_count} records" + logger.debug(f"filter message: {message}") + return message + + @output + @render.table + def penguins_filtered_table(): + filtered_df = reactive_df.get() + return filtered_df + + @output + @render_widget + def penguins_output_widget1(): + df = reactive_df.get() + plotly_plot = px.scatter( + df, + x="bill_length_mm", + y="body_mass_g", + color="species", + title="Penguins Plot (Plotly Express))", + labels={ + "bill_length_mm": "Bill Length (mm)", + "body_mass_g": "Body Mass (g)" + }, + size_max=8 + ) + + return plotly_plot + + # return a list of function names for use in reactive outputs + return [ + penguins_record_count_string, + penguins_filtered_table, + penguins_output_widget1, + ] diff --git a/penguins_ui_inputs.py b/penguins_ui_inputs.py new file mode 100644 index 0000000..9c71d03 --- /dev/null +++ b/penguins_ui_inputs.py @@ -0,0 +1,42 @@ +""" +Purpose: Provide user interaction options for the Penguins dataset. + +Checkboxes should be independent of each other. + +Radio buttons should be mutually exclusive. + +IDs must be unique. They are capitalized in this app for clarity (not typical). +The IDs are case-sensitive and must match the server code exactly. + +We prefaced IDs with the dataset name to avoid naming conflicts. + +""" + +from shiny import ui + + +def get_penguins_sidebar(): + return ui.panel_sidebar( + ui.h2("Penguins Interaction"), + ui.tags.hr(), + ui.input_slider( + "PENGUIN_BODY_MASS_RANGE", + "Body Mass (g)", + min=2700, + max=6300, + value=[2700, 6300], + ), + ui.input_numeric("PENGUIN_MAX_BILL", "Max Bill Length (mm):", value=60.0), + ui.input_checkbox("PENGUIN_SPECIES_Adelie", "Adelie", value=True), + ui.input_checkbox("PENGUIN_SPECIES_Chinstrap", "Chinstrap", value=True), + ui.input_checkbox("PENGUIN_SPECIES_Gentoo", "Gentoo", value=True), + ui.input_radio_buttons( + "PENGUIN_GENDER", + "Select Genders", + {"a": "All (includes missing values)", "f": "Female", "m": "Male"}, + selected="a", + ), + ui.tags.hr(), + ui.p("Please be patient. The charts may take a few seconds to load."), + ui.tags.hr(), + ) diff --git a/penguins_ui_outputs.py b/penguins_ui_outputs.py new file mode 100644 index 0000000..28b4ae8 --- /dev/null +++ b/penguins_ui_outputs.py @@ -0,0 +1,21 @@ +''' +Purpose: Display ouput for the Penguins dataset. +''' +from shiny import ui +from shinywidgets import output_widget + +def get_penguins_main(): + return ui.panel_main( + ui.h2("Main Panel with Reactive Output"), + ui.tags.hr(), + ui.tags.section( + ui.h3("Filtered Penguins: Charts"), + output_widget("penguins_output_widget1"), + ui.tags.hr(), + ui.h3("Filtered Penguins Table"), + ui.output_text("penguins_record_count_string"), + ui.output_table("penguins_filtered_table"), + ui.tags.hr(), + + ) + ) diff --git a/relationships_server.py b/relationships_server.py new file mode 100644 index 0000000..e8fde77 --- /dev/null +++ b/relationships_server.py @@ -0,0 +1,74 @@ +''' +Purpose: Provide reactive output for the relationships dataset. + +Use inputs from the UI Sidebar to filter the dataset. + +Update ouputs in the UI Main Panel. + +See: https://holoviews.org/reference/elements/bokeh/Chord.html + +''' + +from shiny import * +import pandas as pd +import holoviews as hv +from holoviews import opts, dim +from bokeh.sampledata.les_mis import data +import jupyter_bokeh as jbk +from shinywidgets import render_widget + +hv.extension('bokeh') + +from util_logger import setup_logger +logger, logname = setup_logger(__name__) + + +def get_relationships_server_functions(input, output, session): + + # get a pandas dataframe of the links and the nodes + df_links = pd.DataFrame(data['links']) + df_nodes = pd.DataFrame(data['nodes']) + + # get a holoviews dataset of nodes. the 'index' column is the node id + nodes = hv.Dataset(df_nodes, 'index') + + + + @output + @render_widget + def relationships_output_widget1(): + + if (input.RELATIONSHIPS_SHOW_TOGGLE()): + logger.info("UI inputs changed. Updating relationships output widget2") + + # create a chord diagram from the links and the nodes + # value = (5, None) means that only links with a value of 5 or greater will be shown + chord = hv.Chord((df_links, nodes)).select(value=(5, None)) + + # Set some chart options + # Category20 is a color palette + # The edge_color is based on the source node + # The node_color is based on the index (node id) + # The labels are the names of the nodes (from the name column) + chord.opts( + opts.Chord( + cmap='Category20', + edge_cmap='Category20', + edge_color=dim('source').str(), + labels='name', + node_color=dim('index').str(), + title='Les Miserables Relationships 5+ (HoloViews Bokeh)', + width=800, + height=800, + )) + + widget = hv.render(chord, backend='bokeh') + wrapped_widget = jbk.BokehModel(widget) + return wrapped_widget + else: + return None + + # return a list of function names for use in reactive outputs + return [ + relationships_output_widget1, + ] diff --git a/relationships_ui_inputs.py b/relationships_ui_inputs.py new file mode 100644 index 0000000..df71965 --- /dev/null +++ b/relationships_ui_inputs.py @@ -0,0 +1,28 @@ +""" +Purpose: Provide user interaction options for the relationships dataset. + +""" +from shiny import ui +from htmltools import head_content, HTML + +from util_logger import setup_logger + +logger, logname = setup_logger(__name__) + +bokeh_dependency = None +try: + from bokeh.resources import Resources + + bokeh_dependency = head_content(HTML(Resources(mode="inline").render())) +except ImportError: + logger.warn("Could not import bokeh") + + +def get_relationships_sidebar(): + return bokeh_dependency, ui.panel_sidebar( + ui.h2("Relationships Interaction"), + ui.input_switch("RELATIONSHIPS_SHOW_TOGGLE", "Show Charts", value=True), + ui.tags.hr(), + ui.p("Please be patient. The charts may take a few seconds to load."), + ui.tags.hr(), + ) diff --git a/relationships_ui_outputs.py b/relationships_ui_outputs.py new file mode 100644 index 0000000..c53403c --- /dev/null +++ b/relationships_ui_outputs.py @@ -0,0 +1,17 @@ +""" +Purpose: Display ouput for the relationships dataset. +""" +from shiny import ui +from shinywidgets import output_widget + + +def get_relationships_main(): + return ui.panel_main( + ui.h2("Main Panel with Reactive Output"), + ui.tags.hr(), + ui.tags.section( + ui.h3("Relationships: Charts"), + output_widget("relationships_output_widget1"), + ui.tags.hr(), + ), + ) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..a0c4e1f --- /dev/null +++ b/requirements.txt @@ -0,0 +1,15 @@ +shiny +shinyswatch +pandas +openpyxl +jinja2 +matplotlib +seaborn +plotnine +shinywidgets +plotly +holoviews +panel +hvplot +ipyleaflet +jupyter_bokeh \ No newline at end of file diff --git a/util_logger.py b/util_logger.py new file mode 100644 index 0000000..7781f96 --- /dev/null +++ b/util_logger.py @@ -0,0 +1,85 @@ +""" +Purpose: Set up logging once and reuse it. + +Author: Denise Case + +This file automatically records your work so you don't have to. +Analysts and data scientists will work hard once, to be lazy later. +You should be able to reuse this code without modification. +You're also welcome to use it as a template for your own logging. + +""" + +import logging +import pathlib +import platform +import sys +import os +import datetime + +def get_source_directory_path(current_file): + """Returns the absolute path to this source directory.""" + dir = os.path.dirname(os.path.abspath(current_file)) + return dir + +def setup_logger(current_file): + '''Setup a logger to automatically log useful information. + @param current_file: the name of the file reqesting a logger. + @returns: the logger object and the name of the logfile. + ''' + logs_dir = pathlib.Path("logs") + logs_dir.mkdir(exist_ok=True) + + module_name = pathlib.Path(current_file).stem + log_file_name = logs_dir.joinpath(module_name + ".log") + + logger = logging.getLogger(module_name) + logger.setLevel(logging.DEBUG) # Set the root logger level. + + # Create file handler which logs even debug messages. + file_handler = logging.FileHandler(log_file_name, "w") + file_handler.setLevel(logging.DEBUG) + + # Create console handler with a higher log level. + console_handler = logging.StreamHandler() + console_handler.setLevel(logging.INFO) + + # Create formatter and add it to the handlers. + formatter = logging.Formatter('%(asctime)s.%(name)s.%(levelname)s.%(message)s') + file_handler.setFormatter(formatter) + console_handler.setFormatter(formatter) + + # Add the handlers to the logger. + logger.addHandler(file_handler) + logger.addHandler(console_handler) + + divider_string = "=============================================================" + python_version_string = platform.python_version() + today = datetime.date.today() + + logger.info(divider_string) + logger.info(divider_string) + logger.info(f"Today is {today} at {datetime.datetime.now().strftime('%I:%M %p')}") + logger.info(f"This file is running on: {os.name} {platform.system()} {platform.release()}") + logger.info(f"The Python version is: {python_version_string}") + logger.info(f"The active conda environment is: {os.environ.get('CONDA_DEFAULT_ENV') }") + logger.info(f"The active pip environment is: {os.environ.get('PIP_DEFAULT_ENV') }") + logger.info(f"The active environment path is: {sys.prefix}") + logger.info(f"The current working directory is: {os.getcwd()}") + logger.info(f"This source file is in: {get_source_directory_path(current_file)}") + logger.info(divider_string) + logger.info(divider_string) + + return logger, log_file_name + +if __name__ == "__main__": + logger, logname = setup_logger(__file__) + logger.info(f"Starting util_datafun_logger.py") + logger.info(f"Information is logged to: logs/{logname}") + logger.info(f"Ending util_datafun_logger.py") + + # Use built-in open() function to read log file and print it to the terminal + with open(logname, 'r') as file_wrapper: + print(file_wrapper.read()) + + \ No newline at end of file