# ----- IMPORTS ----- # import asyncio from datetime import datetime, date, time from pathlib import Path from pandas import DataFrame from numpy import array from modules import ( DF, LAST_UPDATED, START_DATE, WINDOW_OPEN_DATE, GET_SIGNIFICANT, METADATA, AGENCIES, groupby_agency, groupby_date, add_weeks_to_data, pad_missing_dates, plot_agency, plot_tf, plot_NA, plot_NA, ) from shiny import reactive from shiny.express import input, render, ui # load css styles from external file ui.include_css( Path(__file__).parent.joinpath("www") / "style.css") # ----- CREATE OBJECTS ----- # # this text appears in the browser tab TITLE = "CRA Window Exploratory Dashboard - GW Regulatory Studies Center" # page header above main content HEADER = "Congressional Review Act (CRA) Window Exploratory Dashboard" page_header = ui.HTML( f"""

{HEADER}

""" ) # logo at the top of the sidebar sidebar_logo = ui.HTML( f"""
Regulatory Studies Center logo
""" ) # footer at the bottom of the page FOOTER = f""" ----- © 2024 [GW Regulatory Studies Center](https://go.gwu.edu/regstudies). See our page on the [Congressional Review Act](https://regulatorystudies.columbian.gwu.edu/congressional-review-act) for more information. """ # ----- APP LAYOUT ----- # ui.tags.title(TITLE) page_header # sidebar settings with ui.sidebar(open={"desktop": "open", "mobile": "closed"}, fg="#033C5A"): sidebar_logo with ui.tooltip(placement="right", id="window_tooltip"): ui.input_date("start_date", "Select start of window", value=WINDOW_OPEN_DATE, min=START_DATE, max=date.today()) "The estimated lookback window open date is May 22. This dashboard allows users to explore how different lookback window dates would affect the set of rules available for congressional review. See the notes for more information." with ui.tooltip(placement="right", id="sig_tooltip"): ui.input_select("menu_significant", "Select rule significance", choices=["all", "3f1-significant", "other-significant"], selected="all", multiple=True, size=3) "Rule significance as defined in Executive Order 12866." with ui.tooltip(placement="right", id="agency_tooltip"): ui.input_select("menu_agency", "Select agencies", choices=["all"] + AGENCIES, selected=["all"], multiple=True, size=6) "Select one or more parent-level agencies." # value boxes with summary data with ui.layout_column_wrap(): with ui.value_box(class_="summary-values"): "All final rules" with ui.tooltip(placement="bottom", id="all_tooltip"): @render.text def count_rules(): return f"{filtered_df()['document_number'].count()}" f"Federal Register data last retrieved {date.today()}." with ui.value_box(class_="summary-values"): "Section 3(f)(1) Significant rules" with ui.tooltip(placement="bottom", id="3f1_tooltip"): @render.text def count_3f1_significant(): output = "Not available" if GET_SIGNIFICANT: output = f"{filtered_df()['3f1_significant'].sum()}" return output f"Executive Order 12866 significance data last updated {LAST_UPDATED}." with ui.value_box(class_="summary-values"): "Other Significant rules" with ui.tooltip(placement="bottom", id="other_tooltip"): @render.text def count_other_significant(): output = "Not available" if GET_SIGNIFICANT: output = f"{filtered_df()['other_significant'].sum()}" return output f"Executive Order 12866 significance data last updated {LAST_UPDATED}." # main content with ui.navset_card_underline(title=""): with ui.nav_panel("Rules in detail"): with ui.card(full_screen=True): @render.data_frame def table_rule_detail(): df = filter_significance().copy() df.loc[:, "date"] = df.loc[:, "publication_date"].apply(lambda x: f"{x.date()}") char, limit = " ", 10 df.loc[:, "title"] = df["title"].apply(lambda x: x if len(x.split(char)) < (limit + 1) else f"{char.join(x.split(char)[:limit])}...") df.loc[:, "agencies"] = df["parent_slug"].apply(lambda x: "; ".join(x)) cols = [ "date", "title", "agencies", "3f1_significant", "other_significant", ] return render.DataGrid(df.loc[:, [c for c in cols if c in df.columns]], width="100%") with ui.nav_panel("Over time"): ui.input_select("frequency", "Select frequency", choices=["daily", "weekly", "monthly"], selected="weekly") with ui.layout_columns(): with ui.card(full_screen=True): @render.plot def plot_over_time(value_col: str = "rules"): grouped = get_grouped_data_over_time() values = grouped.loc[:, value_col].to_numpy() count_gte_zero = sum(1 if g > 0 else 0 for g in values) max_val = max(values, default=0) if (max_val < 2) or (count_gte_zero < 2): return plot_NA() else: return plot_tf( grouped, input.frequency(), rule_types=input.menu_significant(), ) with ui.card(full_screen=True): @render.data_frame def table_over_time(): grouped = get_grouped_data_over_time() date_cols = ["publication_date", "week_of", ] if any(d in grouped.columns for d in date_cols): grouped = grouped.astype({d: "str" for d in date_cols if d in grouped.columns}, errors="ignore") grouped = grouped.rename(columns={ "publication_year": "year", "publication_month": "month", "publication_date": "date", }, errors="ignore") cols = [ "date", "year", "month", "week_of", "rules", "3f1_significant", "other_significant", ] return render.DataTable(grouped.loc[:, [c for c in cols if c in grouped.columns]]) with ui.nav_panel("By agency"): with ui.layout_columns(): with ui.card(full_screen=True): @render.plot def plot_by_agency(): grouped = grouped_df_agency() if len(grouped) < 2: return plot_NA() else: plot = plot_agency( grouped.head(10), rule_types=input.menu_significant(), ) return plot with ui.card(full_screen=True): @render.data_frame def table_by_agency(): grouped = grouped_df_agency() cols = [ "agency", "acronym", "rules", "3f1_significant", "other_significant", ] return render.DataTable(grouped.loc[:, [c for c in cols if c in grouped.columns]]) # download data with ui.accordion(open=False): with ui.accordion_panel("Download Data"): @render.download( label="Download data as CSV", filename=f"rules_in_cra_window_accessed_{date.today()}.csv", ) async def download( output_cols: tuple | list = ( "document_number", "citation", "publication_date", "title", "type", "action", "json_url", "html_url", "agencies", "independent_reg_agency", "parent_agencies", "subagencies", "president_id", "significant", "3f1_significant", "other_significant" ) ): filt_df = filtered_df().copy() filt_df.loc[:, "agencies"] = filt_df.loc[:, "agency_slugs"].apply(lambda x: "; ".join(x)) filt_df.loc[:, "parent_agencies"] = filt_df.loc[:, "parent_slug"].apply(lambda x: "; ".join(x)) filt_df.loc[:, "subagencies"] = filt_df.loc[:, "subagency_slug"].apply(lambda x: "; ".join(x)) await asyncio.sleep(0.25) yield filt_df.loc[:, [c for c in output_cols if c in filt_df.columns]].to_csv(index=False) # notes with ui.accordion(open=False): with ui.accordion_panel("Notes"): ui.markdown( f""" The [Congressional Review Act](http://uscode.house.gov/view.xhtml?req=granuleid%3AUSC-prelim-title5-chapter8&saved=%7CKHRpdGxlOjUgc2VjdGlvbjo4MDEgZWRpdGlvbjpwcmVsaW0pIE9SIChncmFudWxlaWQ6VVNDLXByZWxpbS10aXRsZTUtc2VjdGlvbjgwMSk%3D%7CdHJlZXNvcnQ%3D%7C%7C0%7Cfalse%7Cprelim&edition=prelim) (CRA) “lookback window” refers to the period starting [60 working days](https://crsreports.congress.gov/product/pdf/R/R46690#page=8) (either session days in the Senate or legislative days in the House of Representatives) before the current session of Congress adjourns and ending the day the subsequent session of Congress first convenes. Rules that are published in the Federal Register and submitted to Congress during that time period are made available for review in the subsequent session of Congress. Due to the retrospective calculation of the window, lookback window dates prior to Congress adjourning are inherently estimates. Based on the published Congressional calendar for the second session of the 118th Congress, the current lookback window date [estimate](https://www.huntonak.com/the-nickel-report/federal-agencies-face-looming-congressional-review-act-deadline) is **May 22, 2024**. This dashboard allows users to explore how different lookback window dates would affect the set of rules available for congressional review. Rule data are retrieved daily from the [Federal Register API](https://www.federalregister.gov/developers/documentation/api/v1), which publishes new editions of the Federal Register each business day. A list of common agency acronyms is available from the [U.S. Government Manual](https://www.govinfo.gov/content/pkg/GOVMAN-2022-12-31/pdf/GOVMAN-2022-12-31-Commonly-Used-Acronyms-105.pdf). """ ) # footer citation ui.markdown( FOOTER ) # ----- REACTIVE CALCULATIONS ----- # @reactive.calc def filtered_df(agency_column: str = "parent_slug"): filt_df = DF # filter dates try: filt_df = filt_df.loc[filt_df["publication_date"] >= input.start_date()] except TypeError: filt_df = filt_df.loc[filt_df["publication_date"] >= datetime.combine(input.start_date(), time(0, 0))] # filter agencies if (input.menu_agency() is not None) and ("all" not in input.menu_agency()): bool_agency = [True if sum(selected in agency for selected in input.menu_agency()) > 0 else False for agency in filt_df[agency_column]] filt_df = filt_df.loc[bool_agency] # return filtered dataframe return filt_df @reactive.calc def filter_significance(): # get data filtered by date and agency filt_df = filtered_df() # filter significance bool_ = [] if (input.menu_significant() is not None) and ("all" not in input.menu_significant()): if "3f1-significant" in input.menu_significant(): bool_.append((filt_df["3f1_significant"] == 1).to_numpy()) if "other-significant" in input.menu_significant(): bool_.append((filt_df["other_significant"] == 1).to_numpy()) filt_df = filt_df.loc[array(bool_).any(axis=0)] # return filtered dataframe return filt_df @reactive.calc def grouped_df_month(): filt_df = filter_significance() grouped = groupby_date(filt_df, significant=GET_SIGNIFICANT) return grouped @reactive.calc def grouped_df_day(): filt_df = filter_significance() date_col = "publication_date" grouped = groupby_date(filt_df, group_col=date_col, significant=GET_SIGNIFICANT) grouped = pad_missing_dates( grouped, date_col, "days", fill_padded_values={ "rules": 0, "3f1_significant": 0, "other_significant": 0, }) return grouped @reactive.calc def grouped_df_week(): filt_df = filter_significance() filt_df = add_weeks_to_data(filt_df) try: grouped = groupby_date(filt_df, group_col=("week_number", "week_of"), significant=GET_SIGNIFICANT) grouped = pad_missing_dates( grouped, "week_of", how="weeks", fill_padded_values={ "rules": 0, "3f1_significant": 0, "other_significant": 0, }) except KeyError as err: grouped = DataFrame(columns=["week_number", "week_of", "rules", "3f1_significant", "other_significant"]) return grouped @reactive.calc def grouped_df_agency(): filt_df = filter_significance() grouped = groupby_agency(filt_df, metadata=METADATA, significant=GET_SIGNIFICANT) return grouped @reactive.calc def get_grouped_data_over_time(): if input.frequency() == "daily": grouped = grouped_df_day() elif input.frequency() == "monthly": grouped = grouped_df_month() elif input.frequency() == "weekly": grouped = grouped_df_week() else: raise ValueError("Only 'daily', 'monthly', or 'weekly' are valid inputs.") return grouped