diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 0000000..6f385a3 --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,32 @@ +name: Deploy to shinyapps.io with 3.10 + +on: + push: + branches: + - main + +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - name: Check out repository code + uses: actions/checkout@v3 + + - name: Set up Python 3.10 + uses: actions/setup-python@v4 + with: + python-version: '3.10' + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + + - name: Deploy to shinyapps + env: + SHINYAPPS_ACCOUNT: ${{ secrets.SHINYAPPS_ACCOUNT }} + SHINYAPPS_SECRET: ${{ secrets.SHINYAPPS_SECRET }} + SHINYAPPS_TOKEN: ${{ secrets.SHINYAPPS_TOKEN }} + run: | + pip install rsconnect-python + rsconnect deploy shiny . --title cintel-04-reactive diff --git a/.gitignore b/.gitignore index c00f9fa..f6b95d8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,167 +1,47 @@ # This file lists the files and folders that should NOT be committed to GitHub. -# Virtual environment +# Mac files +.DS_Store + +# VS Code files +.vscode/ + +# Python Virtual environment folders .venv/ venv/ +# Jupyter Notebook +.ipynb_checkpoints # 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/ +# ruff linter +.ruff_cache/ diff --git a/README.md b/README.md index 15e1ce9..f1dd0d1 100644 --- a/README.md +++ b/README.md @@ -1,28 +1,11 @@ -# Continuous Intelligence and Interactive Analytics - Initial App +# Continuous Intelligence and Interactive Analytics - Reactive Apps +- Interactive app: [cintel-04-reactive](https://denisecase.shinyapps.io/cintel-04-reactive/) - 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. @@ -41,7 +24,7 @@ Copy this starter repository into your own GitHub account by clicking the 'Fork' ### Make Changes in VS Code -With your respository folder open in VS Code: +With your repository 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. @@ -65,6 +48,14 @@ Details matter - check spelling, capitalization, plurals, spacing when things do - [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) + +----- + +![Flights](./images/01-flights.PNG) + +![MT Cars](./images/02-mtcars.PNG) +![Penguins](./images/03-penguins.PNG) -- [Seaborn Flights Dataset](https://seaborn.pydata.org/tutorial/data_structure.html) \ No newline at end of file +![Relationships](./images/04-relationships.PNG) diff --git a/REQUIREMENTS.md b/REQUIREMENTS.md deleted file mode 100644 index a866585..0000000 --- a/REQUIREMENTS.md +++ /dev/null @@ -1,34 +0,0 @@ -# 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 index 13f982b..c7d6906 100644 --- a/SHINY.md +++ b/SHINY.md @@ -53,25 +53,15 @@ When VS Code asks if it should add the new virtual environment, click yes. - Activate it on Windows: `.venv\Scripts\activate` - Activate it on macOS/Linux `source .venv/bin/` -## Install Libaries into Virtual Environment +## Install Libraries 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 pip wheel python -m pip install --upgrade -r requirements.txt ``` - ## Run the App Verify your virtual environment is activated. Run the app. @@ -81,7 +71,7 @@ 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). +For example, try CTRL 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. @@ -89,4 +79,13 @@ Reopen the terminal window and be sure the virtual environment is activated before running the app again. +----- + +## โš ๏ธ Delete Hosted App Before Pushing to GitHub + +Reminder: The GitHub action deploy.yml may not automatically delete an existing app from shinyapps.io so we can redeploy. + +Before pushing to GitHub, login to [shinyapps.io](https://www.shinyapps.io/) and view the list of applications. +- First archive the app. +- Then delete the archived app. diff --git a/app.py b/app.py index 1efc131..b22d6f1 100644 --- a/app.py +++ b/app.py @@ -1,78 +1,85 @@ """ 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. +interactive analytics dashboard using Shiny for Python with +interactive charts from HoloViews Bokeh and Plotly Express. + +Each Shiny app has two parts: + +- a user interface app_ui object (similar to the HTML in a web page) +- a server function that provides the logic for the app (similar to JS in a web page). """ -from shiny import * +from shiny import App, ui 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 flights_ui_inputs import get_flights_inputs +from flights_ui_outputs import get_flights_outputs 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 mtcars_ui_inputs import get_mtcars_inputs +from mtcars_ui_outputs import get_mtcars_outputs 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 penguins_ui_inputs import get_penguins_inputs +from penguins_ui_outputs import get_penguins_outputs 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 relationships_ui_inputs import get_relationships_inputs +from relationships_ui_outputs import get_relationships_outputs from util_logger import setup_logger logger, logname = setup_logger(__name__) app_ui = ui.page_navbar( - shinyswatch.theme.minty(), + shinyswatch.theme.lumen(), ui.nav( "Flights", ui.layout_sidebar( - get_flights_sidebar(), - get_flights_main(), + get_flights_inputs(), + get_flights_outputs(), ), ), ui.nav( "MT_Cars", ui.layout_sidebar( - get_mtcars_sidebar(), - get_mtcars_main(), + get_mtcars_inputs(), + get_mtcars_outputs(), ), ), ui.nav( "Penguins", ui.layout_sidebar( - get_penguins_sidebar(), - get_penguins_main(), + get_penguins_inputs(), + get_penguins_outputs(), ), ), ui.nav( "Relationships", ui.layout_sidebar( - get_relationships_sidebar(), - get_relationships_main(), + get_relationships_inputs(), + get_relationships_outputs(), ), ), - 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("App", href="https://denisecase.shinyapps.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")), + ui.nav(ui.a("Widgets", href="https://shiny.rstudio.com/py/docs/ipywidgets.html")), title=ui.h1("Case Dashboard"), ) def server(input, output, session): + """Define functions to create UI outputs.""" + 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) + get_flights_server_functions(input, output, session) + get_mtcars_server_functions(input, output, session) + get_penguins_server_functions(input, output, session) + get_relationships_server_functions(input, output, session) + -#app = App(app_ui, server, debug=True) +# app = App(app_ui, server, debug=True) app = App(app_ui, server) diff --git a/app_data.py b/app_data.py deleted file mode 100644 index 683a9c7..0000000 --- a/app_data.py +++ /dev/null @@ -1,42 +0,0 @@ -''' -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/data/app_data.py b/data/app_data.py new file mode 100644 index 0000000..6d3a58b --- /dev/null +++ b/data/app_data.py @@ -0,0 +1,53 @@ +""" +Purpose: Use Python to generate local data files in this folder. + +To create data files, + modify the code to import the dataset in the format you want. + Uncomment those lines and run this script in the terminal with `python app_data.py`. + +After creating the data files, you can comment out code you don't need to re-run. +In VS Code, you can select many lines, and use CTRL + / to comment out all of them. + +CSV files can be easily reviewed in VS Code. + +@imports pathlib to manage directories and file paths +@imports seaborn as sns to load datasets into Pandas DataFrames (df) +@imports pandas as pd to write DataFrames (df) to Excel and CSV files +@imports bokeh.sampledata to load sample datasets into Pandas DataFrames (df) + +mtcars.csv was copied from the Shiny example at: +https://shinylive.io/py/examples/#read-local-csv + + +""" + +import pathlib +import pandas as pd +import seaborn as sns + +from bokeh.sampledata.les_mis import data as les_mis_data + +from util_logger import setup_logger + +logger, logname = setup_logger(__name__) + +# Get a path object representing this data folder. +data_folder = pathlib.Path(__file__).parent + +penguins_df = sns.load_dataset("penguins") +penguins_df.to_excel(data_folder.joinpath("penguins.xlsx")) +penguins_df.to_csv(data_folder.joinpath("penguins.csv")) + +flights_df = sns.load_dataset("flights") +flights_df.to_excel(data_folder.joinpath("flights.xlsx")) +flights_df.to_csv(data_folder.joinpath("flights.csv")) + +mtcars_df = pd.read_csv(data_folder.joinpath("mtcars.csv")) +mtcars_df.to_excel(data_folder.joinpath("mtcars.xlsx")) + +les_mis = pd.DataFrame(les_mis_data["links"]) +les_mis.to_excel("les_mis.xlsx") +les_mis.to_csv("les_mis.csv") + +# url="https://webpath/to/your/data.csv" +# df=pd.read_csv(url) diff --git a/flights.csv b/data/flights.csv similarity index 100% rename from flights.csv rename to data/flights.csv diff --git a/flights.xlsx b/data/flights.xlsx similarity index 100% rename from flights.xlsx rename to data/flights.xlsx diff --git a/mtcars.csv b/data/mtcars.csv similarity index 100% rename from mtcars.csv rename to data/mtcars.csv diff --git a/penguins.csv b/data/penguins.csv similarity index 100% rename from penguins.csv rename to data/penguins.csv diff --git a/penguins.xlsx b/data/penguins.xlsx similarity index 100% rename from penguins.xlsx rename to data/penguins.xlsx diff --git a/data/util_logger.py b/data/util_logger.py new file mode 100644 index 0000000..bda1732 --- /dev/null +++ b/data/util_logger.py @@ -0,0 +1,83 @@ +""" +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 requesting 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(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 environment path is: {sys.prefix}") + logger.info(f"The current working directory is: {os.getcwd()}") + logger.info(divider_string) + + return logger, log_file_name + + +if __name__ == "__main__": + logger, logname = setup_logger(__file__) + logger.info("Starting util_logger.py") + logger.info(f"Information is logged to: logs/{logname}") + logger.info("Ending util_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()) diff --git a/flights_server.py b/flights_server.py index a7e7dbe..c4bf555 100644 --- a/flights_server.py +++ b/flights_server.py @@ -1,11 +1,10 @@ """ -Purpose: Provide reactive output for the Flights dataset. +Purpose: Provide reactive output for Flights dataset. -Use inputs from the UI Sidebar to filter the dataset. +- Use inputs from the UI Sidebar to filter the dataset. +- Update reactive outputs in the UI Main Panel. -Update ouputs in the UI Main Panel. - -Matching the IDs in the UI Sidebar and function/ouput names in the UI Main Panel +Matching the IDs in the UI Sidebar and function/output 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. @@ -14,21 +13,28 @@ - This example is a good starting point - there are many examples online. """ -from shiny import * +import pathlib +from shiny import render, reactive 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): + """Define functions to create UI outputs.""" - original_df = pd.read_excel("flights.xlsx") + p = pathlib.Path(__file__).parent.joinpath("data").joinpath("flights.xlsx") + #logger.info(f"Reading data from {p}") + original_df = pd.read_excel(p) # create new field with year as a string and month together - original_df['year-mon'] = original_df['year'].astype(str) + '-' + original_df['month'] + original_df["year-mon"] = ( + original_df["year"].astype(str) + "-" + original_df["month"] + ) total_count = len(original_df) reactive_df = reactive.Value() @@ -36,10 +42,10 @@ def get_flights_server_functions(input, output, session): @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.''' + """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") + #logger.info("UI inputs changed. Updating flights reactive df") df = original_df.copy() @@ -60,17 +66,16 @@ def _(): input_max = input_range[1] df = df[(df["Date"] >= input_min) & (df["Date"] <= input_max)] - #logger.debug(f"filtered flights df: {df}") + # 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") + #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}") + #logger.debug(f"filter message: {message}") return message @output @@ -84,26 +89,29 @@ def flights_filtered_table(): @render_widget def flights_output_widget1(): df = reactive_df.get() - px_plot = px.scatter(df, - x="year", - y="passengers", + px_plot = px.scatter( + df, + x="year", + y="passengers", title="Flights Scatter Chart (Plotly Express)", - color="month" - ) + 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"}) + 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, diff --git a/flights_ui_inputs.py b/flights_ui_inputs.py index 94127e6..640a08e 100644 --- a/flights_ui_inputs.py +++ b/flights_ui_inputs.py @@ -1,10 +1,9 @@ """ -Purpose: Provide user interaction options for the Flights dataset. +Purpose: Provide user interaction options for 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. +Preface IDs with the dataset name to avoid naming conflicts. See Example date range at https://shinylive.io/py/examples/#date-range-input @@ -16,7 +15,7 @@ from shiny import ui -def get_flights_sidebar(): +def get_flights_inputs(): return ui.panel_sidebar( ui.h2("Flights Interaction"), ui.tags.hr(), @@ -27,6 +26,6 @@ def get_flights_sidebar(): end=date(1960, 12, 31), ), ui.tags.hr(), - ui.p("Please be patient. The charts may take a few seconds to load."), + ui.p("๐Ÿ•’ Please be patient. Outputs may take a few seconds to load."), ui.tags.hr(), ) diff --git a/flights_ui_outputs.py b/flights_ui_outputs.py index 58a6954..7e6cd57 100644 --- a/flights_ui_outputs.py +++ b/flights_ui_outputs.py @@ -1,11 +1,15 @@ -''' -Purpose: Display ouput for the Flights dataset. -''' +""" +Purpose: Display output for Flights dataset. + +@imports shiny.ui as ui +@imports shinywidgets.output_widget for interactive charts +""" from shiny import ui from shinywidgets import output_widget -def get_flights_main(): + +def get_flights_outputs(): return ui.panel_main( ui.h2("Main Panel with Reactive Output"), ui.tags.hr(), @@ -18,5 +22,5 @@ def get_flights_main(): ui.output_text("flights_record_count_string"), ui.output_table("flights_filtered_table"), ui.tags.hr(), - ) + ), ) diff --git a/images/01-flights.PNG b/images/01-flights.PNG new file mode 100644 index 0000000..25e020c Binary files /dev/null and b/images/01-flights.PNG differ diff --git a/images/02-mtcars.PNG b/images/02-mtcars.PNG new file mode 100644 index 0000000..c107d5d Binary files /dev/null and b/images/02-mtcars.PNG differ diff --git a/images/03-penguins.PNG b/images/03-penguins.PNG new file mode 100644 index 0000000..030598f Binary files /dev/null and b/images/03-penguins.PNG differ diff --git a/images/04-relationships.PNG b/images/04-relationships.PNG new file mode 100644 index 0000000..61c28e5 Binary files /dev/null and b/images/04-relationships.PNG differ diff --git a/mtcars_server.py b/mtcars_server.py index f349cc1..94a49b8 100644 --- a/mtcars_server.py +++ b/mtcars_server.py @@ -1,30 +1,32 @@ -''' +""" Purpose: Provide reactive output for the MT Cars dataset. -Use inputs from the UI Sidebar to filter the dataset. +- Use inputs from the UI Sidebar to filter the dataset. +- Update reactive outputs in the UI Main Panel. -Update ouputs in the UI Main Panel. - -Matching the IDs in the UI Sidebar and function/ouput names in the UI Main Panel +Matching the IDs in the UI Sidebar and function/output names in the UI Main Panel to this server code is critical. They are case sensitive and must match exactly. -''' - -from shiny import * +""" +import pathlib +from shiny import render, reactive 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): + """Define functions to create UI outputs.""" - original_df = pd.read_csv("mtcars.csv") + p = pathlib.Path(__file__).parent.joinpath("data").joinpath("mtcars.csv") + #logger.info(f"Reading data from {p}") + original_df = pd.read_csv(p) total_count = len(original_df) reactive_df = reactive.Value() @@ -32,34 +34,32 @@ def get_mtcars_server_functions(input, output, session): @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}") + #logger.debug(f"filter message: {message}") return message @output @@ -75,7 +75,7 @@ def mtcars_output_widget1(): 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(): @@ -88,7 +88,7 @@ def mtcars_plot1(): @output @render.plot def mtcars_plot2(): - df = reactive_df.get() + df = reactive_df.get() plotnine_plot = ( ggplot(df, aes("wt", "mpg")) + geom_point() diff --git a/mtcars_ui_inputs.py b/mtcars_ui_inputs.py index fd7074d..23cfc19 100644 --- a/mtcars_ui_inputs.py +++ b/mtcars_ui_inputs.py @@ -1,16 +1,15 @@ """ -Purpose: Provide user interaction options for the MT Cars dataset. +Purpose: Provide user interaction options for 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. +Preface IDs with the dataset name to avoid naming conflicts. """ from shiny import ui -def get_mtcars_sidebar(): +def get_mtcars_inputs(): return ui.panel_sidebar( ui.h2("MT Cars Interaction"), ui.tags.hr(), @@ -41,6 +40,6 @@ def get_mtcars_sidebar(): ui.output_table("cars_table"), ), ui.tags.hr(), - ui.p("Please be patient. The charts may take a few seconds to load."), + ui.p("๐Ÿ•’ Please be patient. Outputs may take a few seconds to load."), ui.tags.hr(), ) diff --git a/mtcars_ui_outputs.py b/mtcars_ui_outputs.py index 95997a9..aee3719 100644 --- a/mtcars_ui_outputs.py +++ b/mtcars_ui_outputs.py @@ -1,11 +1,14 @@ """ -Purpose: Display ouput for the MT Cars dataset. +Purpose: Display output for MT Cars dataset. + +@imports shiny.ui as ui +@imports shinywidgets.output_widget for interactive charts """ from shiny import ui from shinywidgets import output_widget -def get_mtcars_main(): +def get_mtcars_outputs(): return ui.panel_main( ui.h2("Main Panel with Reactive Output"), ui.tags.hr(), diff --git a/penguins_server.py b/penguins_server.py index 62449e2..a57e87a 100644 --- a/penguins_server.py +++ b/penguins_server.py @@ -1,33 +1,31 @@ -''' +""" Purpose: Provide reactive output for the Penguins dataset. -Use inputs from the UI Sidebar to filter the dataset. +- Use inputs from the UI Sidebar to filter the dataset. +- Update reactive outputs in the UI Main Panel. -Update ouputs in the UI Main Panel. - -Matching the IDs in the UI Sidebar and function/ouput names in the UI Main Panel +Matching the IDs in the UI Sidebar and function/output names in the UI Main Panel to this server code is critical. They are case sensitive and must match exactly. -''' -from shiny import * +""" +import pathlib + +from shiny import render, reactive 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): +logger, logname = setup_logger(__name__) - # Local variables just for this function - # The original dataset and the total record count +def get_penguins_server_functions(input, output, session): + """Define functions to create UI outputs.""" - original_df = pd.read_excel("penguins.xlsx") + p = (pathlib.Path(__file__).parent.joinpath("data").joinpath("penguins.xlsx")) + #logger.info(f"Reading data from {p}") + original_df = pd.read_excel(p) total_count = len(original_df) # Create a reactive value to hold the filtered pandas dataframe @@ -37,15 +35,20 @@ def get_penguins_server_functions(input, output, session): # 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. + @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.''' + It doesn't need a name, because no one calls it directly.""" - logger.info("UI inputs changed. Updating penguins reactive df") + #logger.info("UI inputs changed. Updating penguins reactive df") df = original_df.copy() @@ -53,7 +56,9 @@ def _(): 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) + 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 @@ -79,17 +84,16 @@ def _(): gender_filter = df["sex"] == gender_dict[input_gender] df = df[gender_filter] - #logger.debug(f"filtered penguins df: {df}") + # 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") + #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}") + #logger.debug(f"filter message: {message}") return message @output @@ -102,19 +106,19 @@ def penguins_filtered_table(): @render_widget def penguins_output_widget1(): df = reactive_df.get() - plotly_plot = px.scatter( - df, + 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)" + "body_mass_g": "Body Mass (g)", }, - size_max=8 + size_max=8, ) - + return plotly_plot # return a list of function names for use in reactive outputs diff --git a/penguins_ui_inputs.py b/penguins_ui_inputs.py index 9c71d03..5ccc8ef 100644 --- a/penguins_ui_inputs.py +++ b/penguins_ui_inputs.py @@ -1,21 +1,19 @@ """ Purpose: Provide user interaction options for the Penguins dataset. -Checkboxes should be independent of each other. - -Radio buttons should be mutually exclusive. + - Choose checkboxes when the options are independent of each other. + - Choose radio buttons when a set of options are 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. +Preface IDs with the dataset name to avoid naming conflicts. """ from shiny import ui -def get_penguins_sidebar(): +def get_penguins_inputs(): return ui.panel_sidebar( ui.h2("Penguins Interaction"), ui.tags.hr(), @@ -37,6 +35,6 @@ def get_penguins_sidebar(): selected="a", ), ui.tags.hr(), - ui.p("Please be patient. The charts may take a few seconds to load."), + ui.p("๐Ÿ•’ Please be patient. Outputs may take a few seconds to load."), ui.tags.hr(), ) diff --git a/penguins_ui_outputs.py b/penguins_ui_outputs.py index 28b4ae8..68d8515 100644 --- a/penguins_ui_outputs.py +++ b/penguins_ui_outputs.py @@ -1,10 +1,14 @@ -''' -Purpose: Display ouput for the Penguins dataset. -''' +""" +Purpose: Display output for Penguins dataset. + +@imports shiny.ui as ui +@imports shinywidgets.output_widget for interactive charts +""" from shiny import ui from shinywidgets import output_widget -def get_penguins_main(): + +def get_penguins_outputs(): return ui.panel_main( ui.h2("Main Panel with Reactive Output"), ui.tags.hr(), @@ -16,6 +20,5 @@ def get_penguins_main(): 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 index e8fde77..6dbf35a 100644 --- a/relationships_server.py +++ b/relationships_server.py @@ -1,73 +1,71 @@ -''' +""" Purpose: Provide reactive output for the relationships dataset. -Use inputs from the UI Sidebar to filter the dataset. +- Use inputs from the UI Sidebar to filter the dataset. +- Update reactive outputs in the UI Main Panel. -Update ouputs in the UI Main Panel. +Matching the IDs in the UI Sidebar and function/output names in the UI Main Panel +to this server code is critical. They are case sensitive and must match exactly. 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__) +hv.extension("bokeh") def get_relationships_server_functions(input, output, session): - + """Define functions to create UI outputs.""" + # get a pandas dataframe of the links and the nodes - df_links = pd.DataFrame(data['links']) - df_nodes = pd.DataFrame(data['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') + 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") - 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) only links with a value 5+ will be shown + chord = hv.Chord((df_links, nodes)).select(value=(5, None)) - # 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 Misรฉrables Relationships 5+ (HoloViews Bokeh)", + width=800, + height=800, + ) + ) - # 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 - 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 index df71965..98d33e2 100644 --- a/relationships_ui_inputs.py +++ b/relationships_ui_inputs.py @@ -18,11 +18,11 @@ logger.warn("Could not import bokeh") -def get_relationships_sidebar(): +def get_relationships_inputs(): 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.p("๐Ÿ•’ Please be patient. Outputs may take a few seconds to load."), ui.tags.hr(), ) diff --git a/relationships_ui_outputs.py b/relationships_ui_outputs.py index c53403c..6a56974 100644 --- a/relationships_ui_outputs.py +++ b/relationships_ui_outputs.py @@ -1,11 +1,14 @@ """ -Purpose: Display ouput for the relationships dataset. +Purpose: Display output for relationships dataset. + +@imports shiny.ui as ui +@imports shinywidgets.output_widget for interactive charts """ from shiny import ui from shinywidgets import output_widget -def get_relationships_main(): +def get_relationships_outputs(): return ui.panel_main( ui.h2("Main Panel with Reactive Output"), ui.tags.hr(), diff --git a/requirements.txt b/requirements.txt index a0c4e1f..c4e5222 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,15 +1,18 @@ -shiny -shinyswatch -pandas -openpyxl -jinja2 -matplotlib -seaborn -plotnine -shinywidgets -plotly +htmltools holoviews -panel hvplot ipyleaflet -jupyter_bokeh \ No newline at end of file +ipywidgets +jinja2 +jupyter_bokeh +matplotlib +openpyxl +pandas +panel +plotly +plotnine +rsconnect-python +seaborn +shiny +shinyswatch +shinywidgets diff --git a/util_logger.py b/util_logger.py index 7781f96..bda1732 100644 --- a/util_logger.py +++ b/util_logger.py @@ -17,16 +17,18 @@ 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. - ''' + """Setup a logger to automatically log useful information. + @param current_file: the name of the file requesting a logger. + @returns: the logger object and the name of the logfile. + """ logs_dir = pathlib.Path("logs") logs_dir.mkdir(exist_ok=True) @@ -45,7 +47,7 @@ def setup_logger(current_file): console_handler.setLevel(logging.INFO) # Create formatter and add it to the handlers. - formatter = logging.Formatter('%(asctime)s.%(name)s.%(levelname)s.%(message)s') + formatter = logging.Formatter("%(asctime)s.%(name)s.%(levelname)s %(message)s") file_handler.setFormatter(formatter) console_handler.setFormatter(formatter) @@ -54,32 +56,28 @@ def setup_logger(current_file): logger.addHandler(console_handler) divider_string = "=============================================================" - python_version_string = platform.python_version() + 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"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("Starting util_logger.py") logger.info(f"Information is logged to: logs/{logname}") - logger.info(f"Ending util_datafun_logger.py") + logger.info("Ending util_logger.py") # Use built-in open() function to read log file and print it to the terminal - with open(logname, 'r') as file_wrapper: + with open(logname, "r") as file_wrapper: print(file_wrapper.read()) - - \ No newline at end of file