Mark Febrizio
commited on
Commit
•
c532374
1
Parent(s):
6958bad
Revisions 2024 07 (#16)
Browse files* Update plotting.py
add colors
* update placeholder plot
* update req
* Update requirements.in
* aesthetic improvements
- better titles and labels for plots
- adds colors
- changes action buttons in value boxes
* Update plotting.py
* Update requirements-dev-win.txt
* Update requirements.txt
* more revisions to labels
* Update app.py
add tooltip
- app.py +64 -35
- modules/plotting.py +89 -17
- requirements-dev-mac.txt +5 -5
- requirements-dev-win.txt +8 -8
- requirements.in +1 -1
- requirements.txt +12 -12
- www/style.css +6 -0
app.py
CHANGED
@@ -1,10 +1,12 @@
|
|
|
|
|
|
|
|
1 |
import asyncio
|
2 |
from datetime import datetime, date, time
|
3 |
from pathlib import Path
|
4 |
|
5 |
from faicons import icon_svg
|
6 |
-
from pandas import DataFrame
|
7 |
-
from plotnine import ggplot, labs
|
8 |
|
9 |
from modules import (
|
10 |
DF,
|
@@ -20,65 +22,85 @@ from modules import (
|
|
20 |
pad_missing_dates,
|
21 |
plot_agency,
|
22 |
plot_tf,
|
|
|
23 |
)
|
24 |
|
25 |
from shiny import reactive
|
26 |
from shiny.express import input, render, ui
|
27 |
|
28 |
-
|
|
|
29 |
|
30 |
-
HEADER = "Rules in the Congressional Review Act (CRA) Window"
|
31 |
|
32 |
-
|
33 |
-
-----
|
34 |
-
|
35 |
-
© 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.
|
36 |
-
"""
|
37 |
|
38 |
-
ui.include_css( Path(__file__).parent.joinpath("www") / "style.css")
|
39 |
|
40 |
-
|
|
|
41 |
|
42 |
-
|
|
|
|
|
43 |
f"""
|
44 |
<div class="header">
|
45 |
-
<
|
46 |
-
<img src="logo.png" alt="Regulatory Studies Center logo"/>
|
47 |
-
</a>
|
48 |
</div>
|
49 |
"""
|
50 |
)
|
51 |
|
52 |
-
|
|
|
53 |
f"""
|
54 |
<div class="header">
|
55 |
-
<
|
|
|
|
|
56 |
</div>
|
57 |
"""
|
58 |
)
|
59 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
60 |
page_header
|
61 |
|
|
|
62 |
with ui.sidebar(open={"desktop": "open", "mobile": "closed"}):
|
63 |
sidebar_logo
|
64 |
|
65 |
-
ui.
|
66 |
-
|
|
|
|
|
|
|
|
|
|
|
67 |
ui.input_select("menu_agency", "Select agencies", choices=["all"] + AGENCIES, selected="all", multiple=True)
|
68 |
|
69 |
-
ui.input_select("frequency", "Select frequency", choices=["daily", "
|
70 |
|
71 |
#ui.input_switch("switch", "Stack significant rules in plots", False)
|
72 |
|
|
|
73 |
with ui.layout_column_wrap():
|
74 |
-
with ui.value_box(
|
75 |
"All final rules"
|
76 |
@render.text
|
77 |
def count_rules():
|
78 |
return f"{filtered_df()['document_number'].count()}"
|
79 |
-
ui.input_action_button("filter_all", "
|
80 |
|
81 |
-
with ui.value_box(
|
82 |
"Section 3(f)(1) Significant rules *"
|
83 |
@render.text
|
84 |
def count_3f1_significant():
|
@@ -86,9 +108,9 @@ with ui.layout_column_wrap():
|
|
86 |
if GET_SIGNIFICANT:
|
87 |
output = f"{filtered_df()['3f1_significant'].sum()}"
|
88 |
return output
|
89 |
-
ui.input_action_button("filter_3f1", "
|
90 |
|
91 |
-
with ui.value_box(
|
92 |
"Other Significant rules *"
|
93 |
@render.text
|
94 |
def count_other_significant():
|
@@ -96,14 +118,16 @@ with ui.layout_column_wrap():
|
|
96 |
if GET_SIGNIFICANT:
|
97 |
output = f"{filtered_df()['other_significant'].sum()}"
|
98 |
return output
|
99 |
-
ui.input_action_button("filter_other", "
|
100 |
|
|
|
101 |
ui.markdown(
|
102 |
f"""
|
103 |
\* *Executive Order 12866 significance data last updated **{LAST_UPDATED}***.
|
104 |
"""
|
105 |
)
|
106 |
|
|
|
107 |
with ui.navset_card_underline(title=""):
|
108 |
|
109 |
with ui.nav_panel("Rules in detail"):
|
@@ -136,14 +160,12 @@ with ui.navset_card_underline(title=""):
|
|
136 |
count_gte_zero = sum(1 if g > 0 else 0 for g in values)
|
137 |
max_val = max(values, default=0)
|
138 |
if (max_val < 2) or (count_gte_zero < 2):
|
139 |
-
return (
|
140 |
-
ggplot()
|
141 |
-
+ labs(title="Not enough data available to visualize.")
|
142 |
-
)
|
143 |
else:
|
144 |
return plot_tf(
|
145 |
grouped,
|
146 |
-
input.frequency()
|
|
|
147 |
)
|
148 |
|
149 |
with ui.card(full_screen=True):
|
@@ -180,10 +202,14 @@ with ui.navset_card_underline(title=""):
|
|
180 |
#if input.switch():
|
181 |
# pass
|
182 |
# # placeholder for stacked bar chart
|
183 |
-
|
184 |
-
|
185 |
-
|
186 |
-
|
|
|
|
|
|
|
|
|
187 |
|
188 |
with ui.card(full_screen=True):
|
189 |
@render.data_frame
|
@@ -198,6 +224,7 @@ with ui.navset_card_underline(title=""):
|
|
198 |
]
|
199 |
return render.DataTable(grouped.loc[:, [c for c in cols if c in grouped.columns]])
|
200 |
|
|
|
201 |
with ui.accordion(open=False):
|
202 |
|
203 |
with ui.accordion_panel("Download Data"):
|
@@ -231,6 +258,7 @@ with ui.accordion(open=False):
|
|
231 |
await asyncio.sleep(0.25)
|
232 |
yield filtered_df().loc[:, output_cols].to_csv(index=False)
|
233 |
|
|
|
234 |
with ui.accordion(open=False):
|
235 |
|
236 |
with ui.accordion_panel("Notes"):
|
@@ -243,6 +271,7 @@ with ui.accordion(open=False):
|
|
243 |
"""
|
244 |
)
|
245 |
|
|
|
246 |
ui.markdown(
|
247 |
FOOTER
|
248 |
)
|
|
|
1 |
+
# ----- IMPORTS ----- #
|
2 |
+
|
3 |
+
|
4 |
import asyncio
|
5 |
from datetime import datetime, date, time
|
6 |
from pathlib import Path
|
7 |
|
8 |
from faicons import icon_svg
|
9 |
+
from pandas import DataFrame
|
|
|
10 |
|
11 |
from modules import (
|
12 |
DF,
|
|
|
22 |
pad_missing_dates,
|
23 |
plot_agency,
|
24 |
plot_tf,
|
25 |
+
plot_NA,
|
26 |
)
|
27 |
|
28 |
from shiny import reactive
|
29 |
from shiny.express import input, render, ui
|
30 |
|
31 |
+
# load css styles from external file
|
32 |
+
ui.include_css( Path(__file__).parent.joinpath("www") / "style.css")
|
33 |
|
|
|
34 |
|
35 |
+
# ----- CREATE OBJECTS ----- #
|
|
|
|
|
|
|
|
|
36 |
|
|
|
37 |
|
38 |
+
# this text appears in the browser tab
|
39 |
+
TITLE = "CRA Window Tracker - GW Regulatory Studies Center"
|
40 |
|
41 |
+
# page header above main content
|
42 |
+
HEADER = "Rules in the Congressional Review Act (CRA) Window"
|
43 |
+
page_header = ui.HTML(
|
44 |
f"""
|
45 |
<div class="header">
|
46 |
+
<span>{HEADER}</span>
|
|
|
|
|
47 |
</div>
|
48 |
"""
|
49 |
)
|
50 |
|
51 |
+
# logo at the top of the sidebar
|
52 |
+
sidebar_logo = ui.HTML(
|
53 |
f"""
|
54 |
<div class="header">
|
55 |
+
<a href="https://go.gwu.edu/regstudies" target="_blank">
|
56 |
+
<img src="logo.png" alt="Regulatory Studies Center logo"/>
|
57 |
+
</a>
|
58 |
</div>
|
59 |
"""
|
60 |
)
|
61 |
|
62 |
+
# footer at the bottom of the page
|
63 |
+
FOOTER = f"""
|
64 |
+
-----
|
65 |
+
|
66 |
+
© 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.
|
67 |
+
"""
|
68 |
+
|
69 |
+
|
70 |
+
# ----- APP LAYOUT ----- #
|
71 |
+
|
72 |
+
|
73 |
+
ui.tags.title(TITLE)
|
74 |
+
|
75 |
page_header
|
76 |
|
77 |
+
# sidebar settings
|
78 |
with ui.sidebar(open={"desktop": "open", "mobile": "closed"}):
|
79 |
sidebar_logo
|
80 |
|
81 |
+
#ui.markdown("Estimated CRA window open date: [May 23](https://www.huntonak.com/the-nickel-report/federal-agencies-face-looming-congressional-review-act-deadline)")
|
82 |
+
|
83 |
+
with ui.tooltip(placement="right", id="window_tooltip"):
|
84 |
+
ui.input_date("start_date", "Select start of window", value=WINDOW_OPEN_DATE, min=START_DATE, max=date.today())
|
85 |
+
#ui.span("Card title ", "<>")
|
86 |
+
"The estimated CRA window open date is May 23. See the notes for more information."
|
87 |
+
|
88 |
ui.input_select("menu_agency", "Select agencies", choices=["all"] + AGENCIES, selected="all", multiple=True)
|
89 |
|
90 |
+
ui.input_select("frequency", "Select frequency", choices=["daily", "weekly", "monthly"], selected="weekly")
|
91 |
|
92 |
#ui.input_switch("switch", "Stack significant rules in plots", False)
|
93 |
|
94 |
+
# value boxes with summary data
|
95 |
with ui.layout_column_wrap():
|
96 |
+
with ui.value_box():
|
97 |
"All final rules"
|
98 |
@render.text
|
99 |
def count_rules():
|
100 |
return f"{filtered_df()['document_number'].count()}"
|
101 |
+
ui.input_action_button("filter_all", "Clear Filters", class_="btn-filter", icon=icon_svg("book"))
|
102 |
|
103 |
+
with ui.value_box():
|
104 |
"Section 3(f)(1) Significant rules *"
|
105 |
@render.text
|
106 |
def count_3f1_significant():
|
|
|
108 |
if GET_SIGNIFICANT:
|
109 |
output = f"{filtered_df()['3f1_significant'].sum()}"
|
110 |
return output
|
111 |
+
ui.input_action_button("filter_3f1", "Filter", class_="btn-filter", icon=icon_svg("book"))
|
112 |
|
113 |
+
with ui.value_box():
|
114 |
"Other Significant rules *"
|
115 |
@render.text
|
116 |
def count_other_significant():
|
|
|
118 |
if GET_SIGNIFICANT:
|
119 |
output = f"{filtered_df()['other_significant'].sum()}"
|
120 |
return output
|
121 |
+
ui.input_action_button("filter_other", "Filter", class_="btn-filter", icon=icon_svg("book"))
|
122 |
|
123 |
+
# documentation note on significance data
|
124 |
ui.markdown(
|
125 |
f"""
|
126 |
\* *Executive Order 12866 significance data last updated **{LAST_UPDATED}***.
|
127 |
"""
|
128 |
)
|
129 |
|
130 |
+
# main content
|
131 |
with ui.navset_card_underline(title=""):
|
132 |
|
133 |
with ui.nav_panel("Rules in detail"):
|
|
|
160 |
count_gte_zero = sum(1 if g > 0 else 0 for g in values)
|
161 |
max_val = max(values, default=0)
|
162 |
if (max_val < 2) or (count_gte_zero < 2):
|
163 |
+
return plot_NA()
|
|
|
|
|
|
|
164 |
else:
|
165 |
return plot_tf(
|
166 |
grouped,
|
167 |
+
input.frequency(),
|
168 |
+
rule_type=filter_value.get(),
|
169 |
)
|
170 |
|
171 |
with ui.card(full_screen=True):
|
|
|
202 |
#if input.switch():
|
203 |
# pass
|
204 |
# # placeholder for stacked bar chart
|
205 |
+
if len(grouped) < 2:
|
206 |
+
return plot_NA()
|
207 |
+
else:
|
208 |
+
plot = plot_agency(
|
209 |
+
grouped.head(10),
|
210 |
+
rule_type=filter_value.get(),
|
211 |
+
)
|
212 |
+
return plot
|
213 |
|
214 |
with ui.card(full_screen=True):
|
215 |
@render.data_frame
|
|
|
224 |
]
|
225 |
return render.DataTable(grouped.loc[:, [c for c in cols if c in grouped.columns]])
|
226 |
|
227 |
+
# download data
|
228 |
with ui.accordion(open=False):
|
229 |
|
230 |
with ui.accordion_panel("Download Data"):
|
|
|
258 |
await asyncio.sleep(0.25)
|
259 |
yield filtered_df().loc[:, output_cols].to_csv(index=False)
|
260 |
|
261 |
+
# notes
|
262 |
with ui.accordion(open=False):
|
263 |
|
264 |
with ui.accordion_panel("Notes"):
|
|
|
271 |
"""
|
272 |
)
|
273 |
|
274 |
+
# footer citation
|
275 |
ui.markdown(
|
276 |
FOOTER
|
277 |
)
|
modules/plotting.py
CHANGED
@@ -4,6 +4,9 @@ from plotnine import (
|
|
4 |
aes,
|
5 |
geom_col,
|
6 |
geom_line,
|
|
|
|
|
|
|
7 |
labs,
|
8 |
coord_flip,
|
9 |
scale_x_discrete,
|
@@ -18,7 +21,30 @@ class DataAvailabilityError(Exception):
|
|
18 |
pass
|
19 |
|
20 |
|
21 |
-
def
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
22 |
"""Plot rules by agency.
|
23 |
|
24 |
Args:
|
@@ -30,22 +56,32 @@ def plot_agency(df, group_col = "acronym", value_col = "rules"):
|
|
30 |
ggplot: Plotted data.
|
31 |
"""
|
32 |
order_list = df.loc[:, group_col].to_list()[::-1]
|
|
|
|
|
33 |
|
34 |
plot = (
|
35 |
ggplot(
|
36 |
df,
|
37 |
aes(x=group_col, y=value_col),
|
38 |
)
|
39 |
-
+ geom_col()
|
40 |
+ coord_flip()
|
41 |
+ scale_x_discrete(limits=order_list)
|
42 |
-
+ labs(y=
|
43 |
+ theme_light()
|
44 |
)
|
|
|
45 |
return plot
|
46 |
|
47 |
|
48 |
-
def plot_month(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
49 |
"""Plot rules by month.
|
50 |
|
51 |
Args:
|
@@ -55,24 +91,33 @@ def plot_month(df: DataFrame, group_cols: tuple = ("publication_year", "publicat
|
|
55 |
|
56 |
Returns:
|
57 |
ggplot: Plotted data.
|
58 |
-
"""
|
59 |
df.loc[:, "ym"] = df[group_cols[0]].astype(str) + "-" + df[group_cols[1]].astype(str).str.pad(2, fillchar="0")
|
60 |
order_list = df.loc[:, "ym"].to_list()
|
61 |
-
|
|
|
|
|
62 |
plot = (
|
63 |
ggplot(
|
64 |
df,
|
65 |
aes(x="ym", y=value_col),
|
66 |
)
|
67 |
-
+ geom_col()
|
68 |
+ scale_x_discrete(limits=order_list)
|
69 |
-
+ labs(y=
|
70 |
+ theme_light()
|
71 |
)
|
72 |
return plot
|
73 |
|
74 |
|
75 |
-
def plot_day(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
76 |
"""Plot rules by day.
|
77 |
|
78 |
Args:
|
@@ -94,22 +139,33 @@ def plot_day(df: DataFrame, group_col: str = "publication_date", value_col: str
|
|
94 |
freq = "1 month"
|
95 |
|
96 |
max_value = df.loc[:, value_col].max()
|
|
|
|
|
|
|
97 |
|
98 |
plot = (
|
99 |
ggplot(
|
100 |
df,
|
101 |
aes(x=group_col, y=value_col),
|
102 |
)
|
103 |
-
+ geom_line(group=1)
|
104 |
+ scale_x_datetime(date_breaks=freq, date_labels="%m-%d")
|
105 |
+ scale_y_continuous(limits=(0, max_value), expand=(0, 0, 0.1, 0))
|
106 |
-
+ labs(y=
|
107 |
+ theme_light()
|
108 |
)
|
109 |
return plot
|
110 |
|
111 |
|
112 |
-
def plot_week(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
113 |
max_value = df.loc[:, value_col].max()
|
114 |
|
115 |
date_values = df[group_col].to_list()
|
@@ -128,21 +184,35 @@ def plot_week(df: DataFrame, group_col: str = "week_of", value_col: str = "rules
|
|
128 |
|
129 |
breaks = [val for idx, val in enumerate(date_values) if idx % reduce_by == 0]
|
130 |
|
|
|
|
|
|
|
131 |
plot = (
|
132 |
ggplot(
|
133 |
df,
|
134 |
aes(x=group_col, y=value_col),
|
135 |
)
|
136 |
-
+ geom_line(group=1)
|
137 |
+ scale_x_datetime(breaks=breaks, labels=[f"{w.strftime('%m-%d')}" for w in breaks])
|
138 |
+ scale_y_continuous(limits=(0, max_value), expand=(0, 0, 0.1, 0))
|
139 |
-
+ labs(y=
|
140 |
+ theme_light()
|
141 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
142 |
return plot
|
143 |
|
144 |
|
145 |
-
def plot_tf(df: DataFrame, frequency: str, **kwargs) -> ggplot:
|
146 |
"""Plot rules over time by given frequency.
|
147 |
|
148 |
Args:
|
@@ -160,8 +230,10 @@ def plot_tf(df: DataFrame, frequency: str, **kwargs) -> ggplot:
|
|
160 |
"daily": plot_day,
|
161 |
"weekly": plot_week,
|
162 |
}
|
163 |
-
|
164 |
plot_freq = freq_options.get(frequency, None)
|
165 |
if plot_freq is None:
|
166 |
raise ValueError(f"Frequency must be one of: {', '.join(freq_options.keys())}")
|
167 |
-
|
|
|
|
|
|
|
|
4 |
aes,
|
5 |
geom_col,
|
6 |
geom_line,
|
7 |
+
annotate,
|
8 |
+
theme,
|
9 |
+
element_blank,
|
10 |
labs,
|
11 |
coord_flip,
|
12 |
scale_x_discrete,
|
|
|
21 |
pass
|
22 |
|
23 |
|
24 |
+
def plot_NA(placeholder_text: str = "Not enough data available to visualize.", placeholder_size: int = 14):
|
25 |
+
"""Placeholder plot for when there is not enough data available to visualize."""
|
26 |
+
return (
|
27 |
+
ggplot()
|
28 |
+
+ annotate("text", x=0, y=0, label=placeholder_text, size=placeholder_size)
|
29 |
+
+ theme(axis_ticks=element_blank(), axis_text=element_blank(), panel_grid=element_blank())
|
30 |
+
+ labs(x="", y="", title="")
|
31 |
+
)
|
32 |
+
|
33 |
+
|
34 |
+
def generate_rule_axis_label(rule_type: str | None = None):
|
35 |
+
"""Generate axis label for rules, accounting for rule type ("all", "3f1", or "other")."""
|
36 |
+
y_lab = ""
|
37 |
+
if rule_type is not None:
|
38 |
+
rule_type_options = {
|
39 |
+
"all": "",
|
40 |
+
"3f1": "3(f)(1) Significant",
|
41 |
+
"other": "Other Significant",
|
42 |
+
}
|
43 |
+
y_lab = f"Number of {rule_type_options.get(rule_type)} rules".replace(" ", " ")
|
44 |
+
return y_lab
|
45 |
+
|
46 |
+
|
47 |
+
def plot_agency(df, group_col = "acronym", value_col = "rules", color="#033C5A", rule_type: str | None = None):
|
48 |
"""Plot rules by agency.
|
49 |
|
50 |
Args:
|
|
|
56 |
ggplot: Plotted data.
|
57 |
"""
|
58 |
order_list = df.loc[:, group_col].to_list()[::-1]
|
59 |
+
|
60 |
+
y_lab = generate_rule_axis_label(rule_type)
|
61 |
|
62 |
plot = (
|
63 |
ggplot(
|
64 |
df,
|
65 |
aes(x=group_col, y=value_col),
|
66 |
)
|
67 |
+
+ geom_col(color="#FFFFFF", fill=color)
|
68 |
+ coord_flip()
|
69 |
+ scale_x_discrete(limits=order_list)
|
70 |
+
+ labs(y=y_lab, x="", title="Rules Published by Agency")
|
71 |
+ theme_light()
|
72 |
)
|
73 |
+
|
74 |
return plot
|
75 |
|
76 |
|
77 |
+
def plot_month(
|
78 |
+
df: DataFrame,
|
79 |
+
group_cols: tuple = ("publication_year", "publication_month"),
|
80 |
+
value_col: str = "rules",
|
81 |
+
color: str = "#033C5A",
|
82 |
+
title: str | None = None,
|
83 |
+
y_lab: str = "",
|
84 |
+
):
|
85 |
"""Plot rules by month.
|
86 |
|
87 |
Args:
|
|
|
91 |
|
92 |
Returns:
|
93 |
ggplot: Plotted data.
|
94 |
+
"""
|
95 |
df.loc[:, "ym"] = df[group_cols[0]].astype(str) + "-" + df[group_cols[1]].astype(str).str.pad(2, fillchar="0")
|
96 |
order_list = df.loc[:, "ym"].to_list()
|
97 |
+
if title is None:
|
98 |
+
title = "Rules Published by Month"
|
99 |
+
|
100 |
plot = (
|
101 |
ggplot(
|
102 |
df,
|
103 |
aes(x="ym", y=value_col),
|
104 |
)
|
105 |
+
+ geom_col(color="#FFFFFF", fill=color)
|
106 |
+ scale_x_discrete(limits=order_list)
|
107 |
+
+ labs(y=y_lab, x="", title=title)
|
108 |
+ theme_light()
|
109 |
)
|
110 |
return plot
|
111 |
|
112 |
|
113 |
+
def plot_day(
|
114 |
+
df: DataFrame,
|
115 |
+
group_col: str = "publication_date",
|
116 |
+
value_col: str = "rules",
|
117 |
+
color: str = "#033C5A",
|
118 |
+
title: str | None = None,
|
119 |
+
y_lab: str = "",
|
120 |
+
):
|
121 |
"""Plot rules by day.
|
122 |
|
123 |
Args:
|
|
|
139 |
freq = "1 month"
|
140 |
|
141 |
max_value = df.loc[:, value_col].max()
|
142 |
+
|
143 |
+
if title is None:
|
144 |
+
title = "Rules Published by Date"
|
145 |
|
146 |
plot = (
|
147 |
ggplot(
|
148 |
df,
|
149 |
aes(x=group_col, y=value_col),
|
150 |
)
|
151 |
+
+ geom_line(group=1, color=color)
|
152 |
+ scale_x_datetime(date_breaks=freq, date_labels="%m-%d")
|
153 |
+ scale_y_continuous(limits=(0, max_value), expand=(0, 0, 0.1, 0))
|
154 |
+
+ labs(y=y_lab, x="", title=title)
|
155 |
+ theme_light()
|
156 |
)
|
157 |
return plot
|
158 |
|
159 |
|
160 |
+
def plot_week(
|
161 |
+
df: DataFrame,
|
162 |
+
group_col: str = "week_of",
|
163 |
+
value_col: str = "rules",
|
164 |
+
color: str = "#033C5A",
|
165 |
+
title: str | None = None,
|
166 |
+
y_lab: str = "",
|
167 |
+
show_significant: bool = False,
|
168 |
+
):
|
169 |
max_value = df.loc[:, value_col].max()
|
170 |
|
171 |
date_values = df[group_col].to_list()
|
|
|
184 |
|
185 |
breaks = [val for idx, val in enumerate(date_values) if idx % reduce_by == 0]
|
186 |
|
187 |
+
if title is None:
|
188 |
+
title = "Rules Published by Week"
|
189 |
+
|
190 |
plot = (
|
191 |
ggplot(
|
192 |
df,
|
193 |
aes(x=group_col, y=value_col),
|
194 |
)
|
195 |
+
+ geom_line(group=1, color=color)
|
196 |
+ scale_x_datetime(breaks=breaks, labels=[f"{w.strftime('%m-%d')}" for w in breaks])
|
197 |
+ scale_y_continuous(limits=(0, max_value), expand=(0, 0, 0.1, 0))
|
198 |
+
+ labs(y=y_lab, x="", title=title)
|
199 |
+ theme_light()
|
200 |
)
|
201 |
+
if show_significant:
|
202 |
+
# trying to add significant rules as additional lines
|
203 |
+
# but getting "TypeError: Discrete value supplied to continuous scale"
|
204 |
+
# for 3f1 sig rules
|
205 |
+
df = df.astype({"3f1_significant": "float"})
|
206 |
+
plot = (
|
207 |
+
plot
|
208 |
+
#+ geom_line(aes(x=group_col, y="3f1_significant"), inherit_aes=False, group=1, color="#AA9868", linetype="dotted")
|
209 |
+
+ geom_line(aes(x=group_col, y="other_significant"), inherit_aes=False, group=1, color="#0190DB", linetype="dashed")
|
210 |
+
#+ guide_legend()
|
211 |
+
)
|
212 |
return plot
|
213 |
|
214 |
|
215 |
+
def plot_tf(df: DataFrame, frequency: str, rule_type: str | None = None, **kwargs) -> ggplot:
|
216 |
"""Plot rules over time by given frequency.
|
217 |
|
218 |
Args:
|
|
|
230 |
"daily": plot_day,
|
231 |
"weekly": plot_week,
|
232 |
}
|
|
|
233 |
plot_freq = freq_options.get(frequency, None)
|
234 |
if plot_freq is None:
|
235 |
raise ValueError(f"Frequency must be one of: {', '.join(freq_options.keys())}")
|
236 |
+
|
237 |
+
y_lab = generate_rule_axis_label(rule_type)
|
238 |
+
|
239 |
+
return plot_freq(df, y_lab=y_lab, **kwargs)
|
requirements-dev-mac.txt
CHANGED
@@ -121,7 +121,7 @@ idna==3.7
|
|
121 |
# requests
|
122 |
iniconfig==2.0.0
|
123 |
# via pytest
|
124 |
-
ipykernel==6.29.
|
125 |
# via
|
126 |
# -r requirements-dev.in
|
127 |
# jupyter
|
@@ -150,7 +150,7 @@ json5==0.9.25
|
|
150 |
# via jupyterlab-server
|
151 |
jsonpointer==3.0.0
|
152 |
# via jsonschema
|
153 |
-
jsonschema[format-nongpl]==4.
|
154 |
# via
|
155 |
# jupyter-events
|
156 |
# jupyterlab-server
|
@@ -183,7 +183,7 @@ jupyter-events==0.10.0
|
|
183 |
# via jupyter-server
|
184 |
jupyter-lsp==2.2.5
|
185 |
# via jupyterlab
|
186 |
-
jupyter-server==2.14.
|
187 |
# via
|
188 |
# jupyter-lsp
|
189 |
# jupyterlab
|
@@ -196,7 +196,7 @@ jupyterlab==4.2.3
|
|
196 |
# via notebook
|
197 |
jupyterlab-pygments==0.3.0
|
198 |
# via nbconvert
|
199 |
-
jupyterlab-server==2.27.
|
200 |
# via
|
201 |
# jupyterlab
|
202 |
# notebook
|
@@ -417,7 +417,7 @@ rfc3986-validator==0.1.1
|
|
417 |
# via
|
418 |
# jsonschema
|
419 |
# jupyter-events
|
420 |
-
rpds-py==0.
|
421 |
# via
|
422 |
# jsonschema
|
423 |
# referencing
|
|
|
121 |
# requests
|
122 |
iniconfig==2.0.0
|
123 |
# via pytest
|
124 |
+
ipykernel==6.29.5
|
125 |
# via
|
126 |
# -r requirements-dev.in
|
127 |
# jupyter
|
|
|
150 |
# via jupyterlab-server
|
151 |
jsonpointer==3.0.0
|
152 |
# via jsonschema
|
153 |
+
jsonschema[format-nongpl]==4.23.0
|
154 |
# via
|
155 |
# jupyter-events
|
156 |
# jupyterlab-server
|
|
|
183 |
# via jupyter-server
|
184 |
jupyter-lsp==2.2.5
|
185 |
# via jupyterlab
|
186 |
+
jupyter-server==2.14.2
|
187 |
# via
|
188 |
# jupyter-lsp
|
189 |
# jupyterlab
|
|
|
196 |
# via notebook
|
197 |
jupyterlab-pygments==0.3.0
|
198 |
# via nbconvert
|
199 |
+
jupyterlab-server==2.27.3
|
200 |
# via
|
201 |
# jupyterlab
|
202 |
# notebook
|
|
|
417 |
# via
|
418 |
# jsonschema
|
419 |
# jupyter-events
|
420 |
+
rpds-py==0.19.0
|
421 |
# via
|
422 |
# jsonschema
|
423 |
# referencing
|
requirements-dev-win.txt
CHANGED
@@ -78,7 +78,7 @@ cycler==0.12.1
|
|
78 |
# via
|
79 |
# -c requirements.txt
|
80 |
# matplotlib
|
81 |
-
debugpy==1.8.
|
82 |
# via ipykernel
|
83 |
decorator==5.1.1
|
84 |
# via ipython
|
@@ -125,7 +125,7 @@ idna==3.7
|
|
125 |
# requests
|
126 |
iniconfig==2.0.0
|
127 |
# via pytest
|
128 |
-
ipykernel==6.29.
|
129 |
# via
|
130 |
# -r requirements-dev.in
|
131 |
# jupyter
|
@@ -154,7 +154,7 @@ json5==0.9.25
|
|
154 |
# via jupyterlab-server
|
155 |
jsonpointer==3.0.0
|
156 |
# via jsonschema
|
157 |
-
jsonschema[format-nongpl]==4.
|
158 |
# via
|
159 |
# jupyter-events
|
160 |
# jupyterlab-server
|
@@ -187,7 +187,7 @@ jupyter-events==0.10.0
|
|
187 |
# via jupyter-server
|
188 |
jupyter-lsp==2.2.5
|
189 |
# via jupyterlab
|
190 |
-
jupyter-server==2.14.
|
191 |
# via
|
192 |
# jupyter-lsp
|
193 |
# jupyterlab
|
@@ -196,11 +196,11 @@ jupyter-server==2.14.1
|
|
196 |
# notebook-shim
|
197 |
jupyter-server-terminals==0.5.3
|
198 |
# via jupyter-server
|
199 |
-
jupyterlab==4.2.
|
200 |
# via notebook
|
201 |
jupyterlab-pygments==0.3.0
|
202 |
# via nbconvert
|
203 |
-
jupyterlab-server==2.27.
|
204 |
# via
|
205 |
# jupyterlab
|
206 |
# notebook
|
@@ -339,7 +339,7 @@ prompt-toolkit==3.0.36
|
|
339 |
# ipython
|
340 |
# jupyter-console
|
341 |
# questionary
|
342 |
-
psutil==
|
343 |
# via ipykernel
|
344 |
pure-eval==0.2.2
|
345 |
# via stack-data
|
@@ -422,7 +422,7 @@ rfc3986-validator==0.1.1
|
|
422 |
# via
|
423 |
# jsonschema
|
424 |
# jupyter-events
|
425 |
-
rpds-py==0.
|
426 |
# via
|
427 |
# jsonschema
|
428 |
# referencing
|
|
|
78 |
# via
|
79 |
# -c requirements.txt
|
80 |
# matplotlib
|
81 |
+
debugpy==1.8.2
|
82 |
# via ipykernel
|
83 |
decorator==5.1.1
|
84 |
# via ipython
|
|
|
125 |
# requests
|
126 |
iniconfig==2.0.0
|
127 |
# via pytest
|
128 |
+
ipykernel==6.29.5
|
129 |
# via
|
130 |
# -r requirements-dev.in
|
131 |
# jupyter
|
|
|
154 |
# via jupyterlab-server
|
155 |
jsonpointer==3.0.0
|
156 |
# via jsonschema
|
157 |
+
jsonschema[format-nongpl]==4.23.0
|
158 |
# via
|
159 |
# jupyter-events
|
160 |
# jupyterlab-server
|
|
|
187 |
# via jupyter-server
|
188 |
jupyter-lsp==2.2.5
|
189 |
# via jupyterlab
|
190 |
+
jupyter-server==2.14.2
|
191 |
# via
|
192 |
# jupyter-lsp
|
193 |
# jupyterlab
|
|
|
196 |
# notebook-shim
|
197 |
jupyter-server-terminals==0.5.3
|
198 |
# via jupyter-server
|
199 |
+
jupyterlab==4.2.4
|
200 |
# via notebook
|
201 |
jupyterlab-pygments==0.3.0
|
202 |
# via nbconvert
|
203 |
+
jupyterlab-server==2.27.3
|
204 |
# via
|
205 |
# jupyterlab
|
206 |
# notebook
|
|
|
339 |
# ipython
|
340 |
# jupyter-console
|
341 |
# questionary
|
342 |
+
psutil==6.0.0
|
343 |
# via ipykernel
|
344 |
pure-eval==0.2.2
|
345 |
# via stack-data
|
|
|
422 |
# via
|
423 |
# jsonschema
|
424 |
# jupyter-events
|
425 |
+
rpds-py==0.19.0
|
426 |
# via
|
427 |
# jsonschema
|
428 |
# referencing
|
requirements.in
CHANGED
@@ -5,7 +5,7 @@ fr-toolbelt>=0.1.2, <1.0
|
|
5 |
numpy>=1.26, <2.0
|
6 |
pandas>=2.2, <3.0
|
7 |
plotnine>=0.13.6, <1.0
|
8 |
-
polars>=0.20.
|
9 |
pyarrow>=16.1.0, <17.0
|
10 |
python-dateutil>=2.9.0.post0, <3.0
|
11 |
requests>=2.32.2, <3.0
|
|
|
5 |
numpy>=1.26, <2.0
|
6 |
pandas>=2.2, <3.0
|
7 |
plotnine>=0.13.6, <1.0
|
8 |
+
polars>=0.20.31
|
9 |
pyarrow>=16.1.0, <17.0
|
10 |
python-dateutil>=2.9.0.post0, <3.0
|
11 |
requests>=2.32.2, <3.0
|
requirements.txt
CHANGED
@@ -4,7 +4,7 @@
|
|
4 |
#
|
5 |
# pip-compile requirements.in
|
6 |
#
|
7 |
-
anyio==4.
|
8 |
# via
|
9 |
# starlette
|
10 |
# watchfiles
|
@@ -30,13 +30,13 @@ cycler==0.12.1
|
|
30 |
# via matplotlib
|
31 |
faicons==0.2.2
|
32 |
# via -r requirements.in
|
33 |
-
fonttools==4.
|
34 |
# via matplotlib
|
35 |
fr-toolbelt==0.1.3
|
36 |
# via -r requirements.in
|
37 |
h11==0.14.0
|
38 |
# via uvicorn
|
39 |
-
htmltools==0.5.
|
40 |
# via
|
41 |
# faicons
|
42 |
# shiny
|
@@ -52,13 +52,13 @@ markdown-it-py==3.0.0
|
|
52 |
# via
|
53 |
# mdit-py-plugins
|
54 |
# shiny
|
55 |
-
matplotlib==3.
|
56 |
# via plotnine
|
57 |
mdit-py-plugins==0.4.1
|
58 |
# via shiny
|
59 |
mdurl==0.1.2
|
60 |
# via markdown-it-py
|
61 |
-
mizani==0.11.
|
62 |
# via plotnine
|
63 |
numpy==1.26.4
|
64 |
# via
|
@@ -73,7 +73,7 @@ numpy==1.26.4
|
|
73 |
# pyarrow
|
74 |
# scipy
|
75 |
# statsmodels
|
76 |
-
packaging==24.
|
77 |
# via
|
78 |
# htmltools
|
79 |
# matplotlib
|
@@ -88,11 +88,11 @@ pandas==2.2.2
|
|
88 |
# statsmodels
|
89 |
patsy==0.5.6
|
90 |
# via statsmodels
|
91 |
-
pillow==10.
|
92 |
# via matplotlib
|
93 |
plotnine==0.13.6
|
94 |
# via -r requirements.in
|
95 |
-
polars==
|
96 |
# via -r requirements.in
|
97 |
progress==1.6
|
98 |
# via fr-toolbelt
|
@@ -117,7 +117,7 @@ requests==2.32.3
|
|
117 |
# via
|
118 |
# -r requirements.in
|
119 |
# fr-toolbelt
|
120 |
-
scipy==1.
|
121 |
# via
|
122 |
# mizani
|
123 |
# plotnine
|
@@ -134,7 +134,7 @@ starlette==0.37.2
|
|
134 |
# via shiny
|
135 |
statsmodels==0.14.2
|
136 |
# via plotnine
|
137 |
-
typing-extensions==4.
|
138 |
# via
|
139 |
# htmltools
|
140 |
# shiny
|
@@ -146,9 +146,9 @@ uc-micro-py==1.0.3
|
|
146 |
# via linkify-it-py
|
147 |
urllib3==2.2.2
|
148 |
# via requests
|
149 |
-
uvicorn==0.
|
150 |
# via shiny
|
151 |
-
watchfiles==0.
|
152 |
# via shiny
|
153 |
wcwidth==0.2.13
|
154 |
# via prompt-toolkit
|
|
|
4 |
#
|
5 |
# pip-compile requirements.in
|
6 |
#
|
7 |
+
anyio==4.4.0
|
8 |
# via
|
9 |
# starlette
|
10 |
# watchfiles
|
|
|
30 |
# via matplotlib
|
31 |
faicons==0.2.2
|
32 |
# via -r requirements.in
|
33 |
+
fonttools==4.53.1
|
34 |
# via matplotlib
|
35 |
fr-toolbelt==0.1.3
|
36 |
# via -r requirements.in
|
37 |
h11==0.14.0
|
38 |
# via uvicorn
|
39 |
+
htmltools==0.5.3
|
40 |
# via
|
41 |
# faicons
|
42 |
# shiny
|
|
|
52 |
# via
|
53 |
# mdit-py-plugins
|
54 |
# shiny
|
55 |
+
matplotlib==3.9.1
|
56 |
# via plotnine
|
57 |
mdit-py-plugins==0.4.1
|
58 |
# via shiny
|
59 |
mdurl==0.1.2
|
60 |
# via markdown-it-py
|
61 |
+
mizani==0.11.4
|
62 |
# via plotnine
|
63 |
numpy==1.26.4
|
64 |
# via
|
|
|
73 |
# pyarrow
|
74 |
# scipy
|
75 |
# statsmodels
|
76 |
+
packaging==24.1
|
77 |
# via
|
78 |
# htmltools
|
79 |
# matplotlib
|
|
|
88 |
# statsmodels
|
89 |
patsy==0.5.6
|
90 |
# via statsmodels
|
91 |
+
pillow==10.4.0
|
92 |
# via matplotlib
|
93 |
plotnine==0.13.6
|
94 |
# via -r requirements.in
|
95 |
+
polars==1.2.1
|
96 |
# via -r requirements.in
|
97 |
progress==1.6
|
98 |
# via fr-toolbelt
|
|
|
117 |
# via
|
118 |
# -r requirements.in
|
119 |
# fr-toolbelt
|
120 |
+
scipy==1.14.0
|
121 |
# via
|
122 |
# mizani
|
123 |
# plotnine
|
|
|
134 |
# via shiny
|
135 |
statsmodels==0.14.2
|
136 |
# via plotnine
|
137 |
+
typing-extensions==4.12.2
|
138 |
# via
|
139 |
# htmltools
|
140 |
# shiny
|
|
|
146 |
# via linkify-it-py
|
147 |
urllib3==2.2.2
|
148 |
# via requests
|
149 |
+
uvicorn==0.30.1
|
150 |
# via shiny
|
151 |
+
watchfiles==0.22.0
|
152 |
# via shiny
|
153 |
wcwidth==0.2.13
|
154 |
# via prompt-toolkit
|
www/style.css
CHANGED
@@ -15,3 +15,9 @@
|
|
15 |
font-size: 30px;
|
16 |
vertical-align: middle;
|
17 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
15 |
font-size: 30px;
|
16 |
vertical-align: middle;
|
17 |
}
|
18 |
+
|
19 |
+
.btn-filter {
|
20 |
+
font-size: medium;
|
21 |
+
color: #FFFFFF;
|
22 |
+
background-color: #0190DB;
|
23 |
+
}
|