Spaces:
Build error
Build error
re-organize main script
Browse files
app.py
CHANGED
@@ -1,3 +1,6 @@
|
|
|
|
|
|
|
|
1 |
import pandas as pd
|
2 |
import hvplot.pandas # noqa
|
3 |
import panel as pn
|
@@ -8,22 +11,15 @@ import panel_material_ui as pmu
|
|
8 |
# patch_all()
|
9 |
pn.extension("tabulator", autoreload=True)
|
10 |
|
11 |
-
|
12 |
-
|
13 |
-
|
14 |
-
|
15 |
-
|
16 |
-
|
17 |
-
status_filter = pmu.RadioButtonGroup(
|
18 |
-
label="Issue Status",
|
19 |
-
options=["Open Issues", "Closed Issues", "All Issues"],
|
20 |
-
value="All Issues",
|
21 |
-
size="small",
|
22 |
-
button_type="success",
|
23 |
-
)
|
24 |
-
|
25 |
|
26 |
-
#
|
|
|
|
|
27 |
data_url = (
|
28 |
"https://raw.githubusercontent.com/Azaya89/holoviz-insights/refs/heads/main/data/"
|
29 |
)
|
@@ -51,6 +47,40 @@ release_dfs = {
|
|
51 |
}
|
52 |
|
53 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
54 |
def create_release_plot(df, repo_name):
|
55 |
from packaging.version import parse
|
56 |
|
@@ -78,6 +108,8 @@ def create_release_plot(df, repo_name):
|
|
78 |
# Set "x1" to now for the last release
|
79 |
if not df.empty:
|
80 |
df.loc[df.index[-1], "x1"] = pd.Timestamp.now(tz=df["published_at"].dt.tz)
|
|
|
|
|
81 |
df["y0"] = df["y"].cat.codes - 0.4
|
82 |
df["y1"] = df["y"].cat.codes + 0.4
|
83 |
last_release = df.iloc[-1]
|
@@ -86,9 +118,21 @@ def create_release_plot(df, repo_name):
|
|
86 |
message = f"🔔 Last release was {days_since} days ago on {last_release['published_at'].date()} ({last_release['tag']})"
|
87 |
|
88 |
rects = hv.Rectangles(
|
89 |
-
df[
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
90 |
kdims=["x0", "y0", "x1", "y1"],
|
91 |
-
vdims=["tag", "type", "published_at", "minor_version"],
|
92 |
)
|
93 |
rects = rects.opts(
|
94 |
color="type",
|
@@ -99,9 +143,9 @@ def create_release_plot(df, repo_name):
|
|
99 |
tools=["ycrosshair"],
|
100 |
hover_tooltips=[
|
101 |
("Release Version", "@tag"),
|
102 |
-
("Minor Version", "@minor_version"),
|
103 |
("Release Type", "@type"),
|
104 |
("Release Date", "@published_at"),
|
|
|
105 |
],
|
106 |
xlabel="Date",
|
107 |
ylabel="Minor Version",
|
@@ -117,27 +161,6 @@ def create_release_plot(df, repo_name):
|
|
117 |
)
|
118 |
|
119 |
|
120 |
-
# Helper functions
|
121 |
-
def compute_metrics(df):
|
122 |
-
metrics = {}
|
123 |
-
metrics["first_month"] = df.index[-1].strftime("%B %Y")
|
124 |
-
metrics["last_month"] = df.index[0].strftime("%B %Y")
|
125 |
-
metrics["total_issues"] = len(df)
|
126 |
-
metrics["still_open"] = len(df[df["time_to_close"].isna()])
|
127 |
-
metrics["closed"] = len(df[df["time_to_close"].notna()])
|
128 |
-
metrics["avg_close_time"] = int(df["time_to_close"].mean().days)
|
129 |
-
metrics["median_close_time"] = int(df["time_to_close"].median().days)
|
130 |
-
return metrics
|
131 |
-
|
132 |
-
|
133 |
-
def format_issue_url(url):
|
134 |
-
try:
|
135 |
-
return f'<a href="{url}" target="_blank">{url.split("/")[-1]}</a>'
|
136 |
-
except Exception:
|
137 |
-
return url
|
138 |
-
|
139 |
-
|
140 |
-
# Plots
|
141 |
def create_comparison_plot(df):
|
142 |
monthly_opened = df.resample("ME").size()
|
143 |
monthly_closed = df.dropna(subset=["time_to_close"]).resample("ME").size()
|
@@ -151,7 +174,7 @@ def create_comparison_plot(df):
|
|
151 |
|
152 |
|
153 |
def create_issues_plot(df):
|
154 |
-
# Calculate the number of open issues for each day
|
155 |
df = df.copy()
|
156 |
df["opened_date"] = df.index.normalize()
|
157 |
df["closed_date"] = df["opened_date"] + df["time_to_close"]
|
@@ -174,9 +197,10 @@ def create_issues_plot(df):
|
|
174 |
|
175 |
|
176 |
def create_milestone_plot(df):
|
177 |
-
# Filter to only include open issues
|
178 |
df = df[df["time_to_close"].isna()]
|
179 |
milestone_counts = df["milestone"].value_counts(dropna=False)
|
|
|
180 |
return milestone_counts.hvplot.bar(
|
181 |
title="Open Issues by Milestone",
|
182 |
xlabel="Milestone",
|
@@ -206,28 +230,51 @@ def create_releases_per_year_plot(release_df):
|
|
206 |
release_df = release_df.copy()
|
207 |
release_df["year"] = release_df["published_at"].dt.year
|
208 |
releases_per_year = release_df.groupby("year").size()
|
|
|
209 |
return releases_per_year.hvplot.bar(
|
210 |
xlabel="Year",
|
211 |
ylabel="Number of Releases",
|
212 |
title="Releases per Year",
|
213 |
-
hover_tooltips=[("Year", "@year"),
|
214 |
height=300,
|
215 |
width=600,
|
216 |
)
|
217 |
|
218 |
|
|
|
|
|
|
|
219 |
styles = {
|
220 |
"box-shadow": "rgba(50, 50, 93, 0.25) 0px 6px 12px -2px, rgba(0, 0, 0, 0.3) 0px 3px 7px -3px",
|
221 |
"border-radius": "5px",
|
222 |
"padding": "10px",
|
223 |
}
|
224 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
225 |
|
|
|
|
|
|
|
226 |
@pn.depends(repo_selector)
|
227 |
def indicators_view(repo):
|
228 |
df = repo_dfs[repo]
|
229 |
metrics = compute_metrics(df)
|
230 |
-
|
231 |
pn.indicators.Number(
|
232 |
value=metrics["total_issues"],
|
233 |
name="Total Issues Opened",
|
@@ -258,7 +305,17 @@ def indicators_view(repo):
|
|
258 |
default_color="blue",
|
259 |
styles=styles,
|
260 |
),
|
261 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
262 |
|
263 |
|
264 |
# State variable to store the active tab index
|
@@ -289,27 +346,49 @@ def plots_view(repo):
|
|
289 |
return tabs
|
290 |
|
291 |
|
292 |
-
@pn.depends(repo_selector, status_filter)
|
293 |
-
def table_view(repo, status):
|
294 |
df = repo_dfs[repo].copy()
|
295 |
if status == "Open Issues":
|
296 |
df = df[df["time_to_close"].isna()]
|
297 |
elif status == "Closed Issues":
|
298 |
df = df[df["time_to_close"].notna()]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
299 |
df["issue_no"] = df["html_url"].apply(format_issue_url)
|
300 |
for col in ["time_to_first_response", "time_to_close"]:
|
301 |
df[f"{col}_str"] = df[col].astype(str)
|
302 |
-
|
303 |
-
|
304 |
-
|
305 |
-
|
306 |
-
|
|
|
307 |
"html_url",
|
308 |
"time_to_answer",
|
309 |
"time_in_draft",
|
310 |
"time_to_first_response",
|
311 |
"time_to_close",
|
312 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
313 |
pagination="remote",
|
314 |
page_size=5,
|
315 |
formatters={"issue_no": "html"},
|
@@ -320,23 +399,23 @@ def table_view(repo, status):
|
|
320 |
def header_text(repo):
|
321 |
df = repo_dfs[repo]
|
322 |
metrics = compute_metrics(df)
|
|
|
323 |
text = f"""
|
324 |
## {repo} Dashboard
|
325 |
-
### Issue Metrics from {metrics["first_month"]} to {
|
326 |
"""
|
327 |
return text
|
328 |
|
329 |
|
330 |
-
|
331 |
-
|
|
|
|
|
332 |
icon = pn.widgets.TooltipIcon(value=note)
|
333 |
logo = "https://holoviz.org/_static/holoviz-logo.svg"
|
334 |
|
335 |
logo_pane = pn.pane.Image(logo, width=200, align="center", margin=(10, 0, 10, 0))
|
336 |
|
337 |
-
HEADER_COLOR = "#4199DA"
|
338 |
-
PAPER_COLOR = "#f5f4ef"
|
339 |
-
|
340 |
page = pmu.Page(
|
341 |
main=[
|
342 |
header_text,
|
@@ -352,6 +431,8 @@ page = pmu.Page(
|
|
352 |
repo_selector,
|
353 |
"## Filter by Issue Status",
|
354 |
status_filter,
|
|
|
|
|
355 |
],
|
356 |
title="HoloViz Issue Metrics Dashboard",
|
357 |
theme_config={
|
|
|
1 |
+
# =============================
|
2 |
+
# Imports & Extensions
|
3 |
+
# =============================
|
4 |
import pandas as pd
|
5 |
import hvplot.pandas # noqa
|
6 |
import panel as pn
|
|
|
11 |
# patch_all()
|
12 |
pn.extension("tabulator", autoreload=True)
|
13 |
|
14 |
+
# =============================
|
15 |
+
# Constants & Theme Config
|
16 |
+
# =============================
|
17 |
+
HEADER_COLOR = "#4199DA"
|
18 |
+
PAPER_COLOR = "#f5f4ef"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
19 |
|
20 |
+
# =============================
|
21 |
+
# Data Loading
|
22 |
+
# =============================
|
23 |
data_url = (
|
24 |
"https://raw.githubusercontent.com/Azaya89/holoviz-insights/refs/heads/main/data/"
|
25 |
)
|
|
|
47 |
}
|
48 |
|
49 |
|
50 |
+
# =============================
|
51 |
+
# Helper Functions
|
52 |
+
# =============================
|
53 |
+
def format_issue_url(url):
|
54 |
+
try:
|
55 |
+
return f'<a href="{url}" target="_blank">{url.split("/")[-1]}</a>'
|
56 |
+
except Exception:
|
57 |
+
return url
|
58 |
+
|
59 |
+
|
60 |
+
# =============================
|
61 |
+
# Metric Computation
|
62 |
+
# =============================
|
63 |
+
def compute_metrics(df):
|
64 |
+
metrics = {}
|
65 |
+
metrics["first_month"] = df.index[-1].strftime("%B %Y")
|
66 |
+
metrics["last_month"] = df.index[0].strftime("%B %Y")
|
67 |
+
metrics["total_issues"] = len(df)
|
68 |
+
open_issues = df[df["time_to_close"].isna()]
|
69 |
+
metrics["still_open"] = len(open_issues)
|
70 |
+
metrics["closed"] = len(df) - len(open_issues)
|
71 |
+
metrics["avg_close_time"] = int(df["time_to_close"].mean().days)
|
72 |
+
metrics["median_close_time"] = int(df["time_to_close"].median().days)
|
73 |
+
if "maintainer_responded" in df.columns:
|
74 |
+
awaiting = open_issues[~open_issues["maintainer_responded"].fillna(False)]
|
75 |
+
metrics["open_awaiting_maintainer"] = len(awaiting)
|
76 |
+
else:
|
77 |
+
metrics["open_awaiting_maintainer"] = None
|
78 |
+
return metrics
|
79 |
+
|
80 |
+
|
81 |
+
# =============================
|
82 |
+
# Plot Functions
|
83 |
+
# =============================
|
84 |
def create_release_plot(df, repo_name):
|
85 |
from packaging.version import parse
|
86 |
|
|
|
108 |
# Set "x1" to now for the last release
|
109 |
if not df.empty:
|
110 |
df.loc[df.index[-1], "x1"] = pd.Timestamp.now(tz=df["published_at"].dt.tz)
|
111 |
+
# Add release_span in days
|
112 |
+
df["release_span"] = (df["x1"] - df["x0"]).dt.days
|
113 |
df["y0"] = df["y"].cat.codes - 0.4
|
114 |
df["y1"] = df["y"].cat.codes + 0.4
|
115 |
last_release = df.iloc[-1]
|
|
|
118 |
message = f"🔔 Last release was {days_since} days ago on {last_release['published_at'].date()} ({last_release['tag']})"
|
119 |
|
120 |
rects = hv.Rectangles(
|
121 |
+
df[
|
122 |
+
[
|
123 |
+
"x0",
|
124 |
+
"y0",
|
125 |
+
"x1",
|
126 |
+
"y1",
|
127 |
+
"tag",
|
128 |
+
"type",
|
129 |
+
"published_at",
|
130 |
+
"minor_version",
|
131 |
+
"release_span",
|
132 |
+
]
|
133 |
+
],
|
134 |
kdims=["x0", "y0", "x1", "y1"],
|
135 |
+
vdims=["tag", "type", "published_at", "minor_version", "release_span"],
|
136 |
)
|
137 |
rects = rects.opts(
|
138 |
color="type",
|
|
|
143 |
tools=["ycrosshair"],
|
144 |
hover_tooltips=[
|
145 |
("Release Version", "@tag"),
|
|
|
146 |
("Release Type", "@type"),
|
147 |
("Release Date", "@published_at"),
|
148 |
+
("Release Span (days)", "@release_span"),
|
149 |
],
|
150 |
xlabel="Date",
|
151 |
ylabel="Minor Version",
|
|
|
161 |
)
|
162 |
|
163 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
164 |
def create_comparison_plot(df):
|
165 |
monthly_opened = df.resample("ME").size()
|
166 |
monthly_closed = df.dropna(subset=["time_to_close"]).resample("ME").size()
|
|
|
174 |
|
175 |
|
176 |
def create_issues_plot(df):
|
177 |
+
# Calculate the number of open issues for each day
|
178 |
df = df.copy()
|
179 |
df["opened_date"] = df.index.normalize()
|
180 |
df["closed_date"] = df["opened_date"] + df["time_to_close"]
|
|
|
197 |
|
198 |
|
199 |
def create_milestone_plot(df):
|
200 |
+
# Filter to only include open issues
|
201 |
df = df[df["time_to_close"].isna()]
|
202 |
milestone_counts = df["milestone"].value_counts(dropna=False)
|
203 |
+
milestone_counts.name = "Milestone Issues"
|
204 |
return milestone_counts.hvplot.bar(
|
205 |
title="Open Issues by Milestone",
|
206 |
xlabel="Milestone",
|
|
|
230 |
release_df = release_df.copy()
|
231 |
release_df["year"] = release_df["published_at"].dt.year
|
232 |
releases_per_year = release_df.groupby("year").size()
|
233 |
+
releases_per_year.name = "Releases"
|
234 |
return releases_per_year.hvplot.bar(
|
235 |
xlabel="Year",
|
236 |
ylabel="Number of Releases",
|
237 |
title="Releases per Year",
|
238 |
+
hover_tooltips=[("Year", "@year"), "@Releases"],
|
239 |
height=300,
|
240 |
width=600,
|
241 |
)
|
242 |
|
243 |
|
244 |
+
# =============================
|
245 |
+
# UI Components (Filters, Selectors, etc.)
|
246 |
+
# =============================
|
247 |
styles = {
|
248 |
"box-shadow": "rgba(50, 50, 93, 0.25) 0px 6px 12px -2px, rgba(0, 0, 0, 0.3) 0px 3px 7px -3px",
|
249 |
"border-radius": "5px",
|
250 |
"padding": "10px",
|
251 |
}
|
252 |
|
253 |
+
maintainer_filter = pmu.RadioButtonGroup(
|
254 |
+
label="Maintainer Response",
|
255 |
+
options=["All", "Awaiting Maintainer Response", "Maintainer Responded"],
|
256 |
+
value="All",
|
257 |
+
size="small",
|
258 |
+
button_type="primary",
|
259 |
+
)
|
260 |
+
|
261 |
+
status_filter = pmu.RadioButtonGroup(
|
262 |
+
label="Issue Status",
|
263 |
+
options=["Open Issues", "Closed Issues", "All Issues"],
|
264 |
+
value="All Issues",
|
265 |
+
size="small",
|
266 |
+
button_type="success",
|
267 |
+
)
|
268 |
+
|
269 |
|
270 |
+
# =============================
|
271 |
+
# Views (Indicators, Plots, Table, Header)
|
272 |
+
# =============================
|
273 |
@pn.depends(repo_selector)
|
274 |
def indicators_view(repo):
|
275 |
df = repo_dfs[repo]
|
276 |
metrics = compute_metrics(df)
|
277 |
+
indicators = [
|
278 |
pn.indicators.Number(
|
279 |
value=metrics["total_issues"],
|
280 |
name="Total Issues Opened",
|
|
|
305 |
default_color="blue",
|
306 |
styles=styles,
|
307 |
),
|
308 |
+
]
|
309 |
+
if metrics["open_awaiting_maintainer"] is not None:
|
310 |
+
indicators.append(
|
311 |
+
pn.indicators.Number(
|
312 |
+
value=metrics["open_awaiting_maintainer"],
|
313 |
+
name="Awaiting Maintainer Response",
|
314 |
+
default_color="orange",
|
315 |
+
styles=styles,
|
316 |
+
)
|
317 |
+
)
|
318 |
+
return pmu.FlexBox(*indicators)
|
319 |
|
320 |
|
321 |
# State variable to store the active tab index
|
|
|
346 |
return tabs
|
347 |
|
348 |
|
349 |
+
@pn.depends(repo_selector, status_filter, maintainer_filter)
|
350 |
+
def table_view(repo, status, maintainer_resp):
|
351 |
df = repo_dfs[repo].copy()
|
352 |
if status == "Open Issues":
|
353 |
df = df[df["time_to_close"].isna()]
|
354 |
elif status == "Closed Issues":
|
355 |
df = df[df["time_to_close"].notna()]
|
356 |
+
# Filter by maintainer response
|
357 |
+
if "maintainer_responded" in df.columns and maintainer_resp != "All":
|
358 |
+
mask = df["maintainer_responded"].fillna(False)
|
359 |
+
if maintainer_resp == "Awaiting Maintainer Response":
|
360 |
+
df = df[~mask]
|
361 |
+
elif maintainer_resp == "Maintainer Responded":
|
362 |
+
df = df[mask]
|
363 |
df["issue_no"] = df["html_url"].apply(format_issue_url)
|
364 |
for col in ["time_to_first_response", "time_to_close"]:
|
365 |
df[f"{col}_str"] = df[col].astype(str)
|
366 |
+
# Show maintainer_responded as a column
|
367 |
+
if "maintainer_responded" in df.columns:
|
368 |
+
df["Maintainer Responded"] = df["maintainer_responded"].map(
|
369 |
+
{True: "Yes", False: "No"}
|
370 |
+
)
|
371 |
+
hidden_cols = [
|
372 |
"html_url",
|
373 |
"time_to_answer",
|
374 |
"time_in_draft",
|
375 |
"time_to_first_response",
|
376 |
"time_to_close",
|
377 |
+
"maintainer_responded",
|
378 |
+
]
|
379 |
+
else:
|
380 |
+
hidden_cols = [
|
381 |
+
"html_url",
|
382 |
+
"time_to_answer",
|
383 |
+
"time_in_draft",
|
384 |
+
"time_to_first_response",
|
385 |
+
"time_to_close",
|
386 |
+
]
|
387 |
+
return pn.widgets.Tabulator(
|
388 |
+
df,
|
389 |
+
sizing_mode="stretch_width",
|
390 |
+
name="Table",
|
391 |
+
hidden_columns=hidden_cols,
|
392 |
pagination="remote",
|
393 |
page_size=5,
|
394 |
formatters={"issue_no": "html"},
|
|
|
399 |
def header_text(repo):
|
400 |
df = repo_dfs[repo]
|
401 |
metrics = compute_metrics(df)
|
402 |
+
latest_date = df.index[0].strftime("%B %d, %Y")
|
403 |
text = f"""
|
404 |
## {repo} Dashboard
|
405 |
+
### Issue Metrics from {metrics["first_month"]} to {latest_date}
|
406 |
"""
|
407 |
return text
|
408 |
|
409 |
|
410 |
+
# =============================
|
411 |
+
# Page Layout & App Launch
|
412 |
+
# =============================
|
413 |
+
note = """The issue metrics shown here are not a full historical record, but represent a snapshot collected automatically at the start of each month.\n Data covers issues from the stated start date up to the stated end date, and is refreshed at the beginning of every new month."""
|
414 |
icon = pn.widgets.TooltipIcon(value=note)
|
415 |
logo = "https://holoviz.org/_static/holoviz-logo.svg"
|
416 |
|
417 |
logo_pane = pn.pane.Image(logo, width=200, align="center", margin=(10, 0, 10, 0))
|
418 |
|
|
|
|
|
|
|
419 |
page = pmu.Page(
|
420 |
main=[
|
421 |
header_text,
|
|
|
431 |
repo_selector,
|
432 |
"## Filter by Issue Status",
|
433 |
status_filter,
|
434 |
+
"## Maintainer Response",
|
435 |
+
maintainer_filter,
|
436 |
],
|
437 |
title="HoloViz Issue Metrics Dashboard",
|
438 |
theme_config={
|