Skip to content

Commit 4e7d4a6

Browse files
authored
Merge pull request #5 from ga4gh/dc-ui-changes
DC UI changes
2 parents a40e840 + 77a54b4 commit 4e7d4a6

23 files changed

Lines changed: 3290 additions & 1025 deletions

app/assets/logo-tower-white.svg

Lines changed: 522 additions & 0 deletions
Loading

app/assets/style.css

Lines changed: 400 additions & 3 deletions
Large diffs are not rendered by default.

app/callbacks/epmc_callbacks.py

Lines changed: 446 additions & 50 deletions
Large diffs are not rendered by default.

app/callbacks/github_callbacks.py

Lines changed: 317 additions & 50 deletions
Large diffs are not rendered by default.

app/callbacks/home_callbacks.py

Lines changed: 29 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,9 @@
1-
from dash import dash, Input, Output
2-
import pandas as pd
3-
import plotly.express as px
4-
5-
from app import app
6-
from app.services.pypi_client import get_pypi_details
1+
from dash import Input, Output, State, ctx
2+
from dash import html
3+
from app.services.pypi_client import get_pypi_details, get_total_packages
74
import dash_bootstrap_components as dbc
8-
import plotly.graph_objects as go
9-
import plotly.express as px
10-
from dash import html, dcc, dash_table
11-
import pandas as pd
12-
from app.pages.pypi.pypi import layout as pypi_layout
13-
from app.pages.github.github import layout as github_layout
145

15-
from dash import ctx
166
from app.layouts.pypi_layout import get_pypi_layout
17-
from app.services.pypi_client import get_pypi_details, get_total_packages
187

198
from app.layouts.github_layout import get_github_layout
209
from app.services.github_client import prepare_github_data
@@ -28,23 +17,19 @@ def register_home_callbacks(app):
2817
Output("module-content", "children"),
2918
Output("open-pypi", "children"),
3019
Output("open-github", "children"),
31-
Output("open-pubmed", "children"),
3220
Output("open-epmc", "children"),
3321
Input("open-pypi", "n_clicks"),
3422
Input("open-github", "n_clicks"),
35-
Input("open-pubmed", "n_clicks"),
3623
Input("open-epmc", "n_clicks"),
3724
prevent_initial_call=True
3825
)
39-
def display_module(pypi_click, github_click, pubmed_click, epmc_click):
26+
def display_module(pypi_click, github_click, epmc_click):
4027

4128
trigger = ctx.triggered_id
42-
43-
button_id = ctx.triggered[0]["prop_id"].split(".")[0]
29+
4430
# Default button text
4531
pypi_text = "Open"
4632
github_text = "Open"
47-
pubmed_text = "Open"
4833
epmc_text = "Open"
4934

5035
if trigger == "open-pypi":
@@ -55,24 +40,40 @@ def display_module(pypi_click, github_click, pubmed_click, epmc_click):
5540
pypi_text = "Close"
5641

5742
elif trigger == "open-github":
43+
gh_data = prepare_github_data()
5844
content = get_github_layout(
59-
prepare_github_data()[0], # gh_df
60-
prepare_github_data()[4] # total_repositories
45+
gh_data[0], # gh_df
46+
gh_data[4], # total_repositories,
47+
gh_data[5] # workstreams
6148
)
6249
github_text = "Close"
6350

64-
elif trigger == "open-pubmed":
65-
pubmed_text = "Close"
66-
return html.Div("PubMed module coming soon"), pypi_text, github_text, pubmed_text, epmc_text
67-
6851
elif trigger == "open-epmc":
6952
epmc_data = prepare_epmc_data()
7053
content = get_epmc_layout(
7154
epmc_data[0], # entries_df
7255
epmc_data[1], # countries_df
7356
epmc_data[2], # authors_df
74-
epmc_data[3], # total_entries
57+
epmc_data[3], # total_entries,
58+
epmc_data[4], # citations
7559
)
7660
epmc_text = "Close"
7761

78-
return content, pypi_text, github_text, pubmed_text, epmc_text
62+
return content, pypi_text, github_text, epmc_text
63+
64+
@app.callback(
65+
Output("collapse", "is_open"),
66+
Output("collapse-button", "children"),
67+
Input("collapse-button", "n_clicks"),
68+
State("collapse", "is_open"),
69+
)
70+
def toggle_collapse(n, is_open):
71+
new_state = not is_open if n else is_open
72+
73+
label = [
74+
html.Span("▼ " if new_state else "▶ ", style={"fontSize": "12px", "marginRight": "4px"}),
75+
html.Span("Show methods and terms "),
76+
html.Span("▼", style={"fontSize": "12px"}),
77+
]
78+
79+
return new_state, label

app/callbacks/pypi_callbacks.py

Lines changed: 29 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,23 @@
1-
from dash import dash, Input, Output
1+
from dash import Input, Output
22
import pandas as pd
33
import plotly.express as px
44

55
from app.services.pypi_client import get_pypi_details
66
import dash_bootstrap_components as dbc
7-
import plotly.graph_objects as go
8-
import plotly.express as px
9-
from dash import html, dcc, dash_table
10-
import pandas as pd
7+
from dash import html, dcc
8+
9+
10+
PYPI_CATEGORY_COLORS = {
11+
"Implementation": "#1f77b4",
12+
"GA4GH Standard": "#ff7f0e",
13+
"GA4GH mentions": "#2ca02c",
14+
}
1115

1216
def register_pypi_callbacks(app):
1317

18+
# Cache PyPI data once at registration time
19+
_pypi_df = get_pypi_details()
20+
1421
# -----------------------
1522
# DataTable search (unchanged)
1623
# -----------------------
@@ -19,13 +26,14 @@ def register_pypi_callbacks(app):
1926
Input('table-search', 'value')
2027
)
2128
def update_table(search_value):
22-
df = get_pypi_details()
29+
df = _pypi_df
2330
if not search_value:
2431
return df.reset_index().to_dict('records')
25-
filtered = df.reset_index()[df.reset_index().apply(
26-
lambda row: row.astype(str).str.contains(search_value, case=True).any(), axis=1
27-
)]
28-
return filtered.to_dict('records')
32+
indexed = df.reset_index()
33+
mask = indexed.apply(
34+
lambda col: col.astype(str).str.contains(search_value, case=False, na=False)
35+
).any(axis=1)
36+
return indexed[mask].to_dict('records')
2937

3038
# -----------------------
3139
# Update bar chart based on filters
@@ -38,7 +46,7 @@ def update_table(search_value):
3846
Input("top-n-slider", "value")
3947
)
4048
def update_bar(author_filter, email_filter, category_filter, top_n):
41-
df = get_pypi_details()
49+
df = _pypi_df
4250
dff = df.copy()
4351

4452
# Apply filters
@@ -59,7 +67,7 @@ def update_bar(author_filter, email_filter, category_filter, top_n):
5967
y="versions_count",
6068
color="category",
6169
hover_data=["project_name", "category", "versions_count"],
62-
color_discrete_sequence=px.colors.qualitative.Safe,
70+
color_discrete_map=PYPI_CATEGORY_COLORS,
6371
category_orders={
6472
"project_name": dff["project_name"].tolist() # 👈 THIS FIXES IT
6573
}
@@ -102,7 +110,7 @@ def update_bar(author_filter, email_filter, category_filter, top_n):
102110
Input("filter-category", "value")
103111
)
104112
def update_category_distribution(author_filter, email_filter, category_filter):
105-
df = get_pypi_details()
113+
df = _pypi_df
106114
dff = df.copy()
107115

108116
# Apply filters
@@ -126,7 +134,13 @@ def update_category_distribution(author_filter, email_filter, category_filter):
126134
"type": "pie",
127135
"hole": 0.4,
128136
"textinfo": "label+percent",
129-
"hoverinfo": "label+value+percent"
137+
"hoverinfo": "label+value+percent",
138+
"marker": {
139+
"colors": [
140+
PYPI_CATEGORY_COLORS.get(cat, "#9aa0a6")
141+
for cat in cat_counts["category"]
142+
]
143+
},
130144
}],
131145
"layout": {
132146
"title": {"text": "Category Distribution", "x": 0.5, "xanchor": "center", "font": {"size": 20, "color": "#2C3E50"}},
@@ -144,7 +158,7 @@ def show_project_details(selected_rows):
144158

145159
if not selected_rows:
146160
return dbc.Alert("Select a project to see details", color="info")
147-
pypi_details = get_pypi_details()
161+
pypi_details = _pypi_df
148162
project = pypi_details.iloc[selected_rows[0]]
149163
github_url = project.get("github_url")
150164
versions_count = project.get("versions_count")

app/constants/api.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
#BASE API endpoint
66
BASE_API = "http://AnalyticsDashboardAlbBalancer-1386294349.us-east-2.elb.amazonaws.com:8000"
7+
#BASE_API = "http://0.0.0.0:8000" # local test url
78

89
# PyPI API endpoint
910
ALL_PACKAGES_API = BASE_API + "/pypi/all-packages"
@@ -23,9 +24,11 @@
2324
PM_KEYWORD = f"{BASE_API}/pubmed/articles/GA4GH"
2425

2526
# EPMC API endpoint
26-
27-
EPMC_BASE_API = "http://0.0.0.0:8000" # local test url
2827
EPMC_ALL_LATEST_ENTRIES = BASE_API + "/epmc/all-latest-entries"
2928
EPMC_AFFILIATION_COUNTRIES_COUNT = BASE_API + "/epmc/affiliation-countries-count"
3029
EPMC_ALL_PMC_AUTHORS = BASE_API + "/epmc/all-pmc-authors"
31-
EPMC_UNIQUE_CITATIONS = BASE_API + "/epmc/unique-citations"
30+
EPMC_UNIQUE_AUTHOR_COUNT = BASE_API + "/epmc/unique-authors-count"
31+
EPMC_TOP_AUTHORS = BASE_API + "/epmc/top-authors"
32+
EPMC_ALL_ARTICLES = BASE_API + "/epmc/all-articles"
33+
EPMC_GET_AUTHORS_BY_ARTICLE = BASE_API + "/epmc/get-authors-by-article-id/"
34+
EPMC_CITATION_OVER_YEARS = BASE_API + "/epmc/citations-over-years"

app/layouts/combined_layout.py

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
import pandas as pd
2+
import plotly.express as px
3+
import dash_bootstrap_components as dbc
4+
from dash import dcc, html
5+
6+
7+
def _build_source_year_df(df, year_col, item_col, source_name):
8+
"""Return normalized [year, item, Source] rows for one source."""
9+
if df is None or df.empty or year_col not in df.columns or item_col not in df.columns:
10+
return pd.DataFrame(columns=["year", "item", "Source"])
11+
12+
tmp = df[[year_col, item_col]].copy()
13+
tmp[year_col] = pd.to_numeric(tmp[year_col], errors="coerce")
14+
tmp[item_col] = tmp[item_col].fillna("").astype(str).str.strip()
15+
tmp = tmp.dropna(subset=[year_col])
16+
tmp = tmp[tmp[item_col] != ""]
17+
18+
if tmp.empty:
19+
return pd.DataFrame(columns=["year", "item", "Source"])
20+
21+
tmp["year"] = tmp[year_col].astype(int)
22+
tmp["item"] = tmp[item_col]
23+
tmp["Source"] = source_name
24+
return tmp[["year", "item", "Source"]]
25+
26+
def _make_source_growth_figure(
27+
source_year_df,
28+
source_name,
29+
line_color,
30+
yearly_label="New repos created",
31+
cumulative_label="Total repos to date",
32+
):
33+
"""Build one cumulative growth chart for a single source."""
34+
if source_year_df is None or source_year_df.empty:
35+
return px.line(title=f"No {source_name} time-series data available")
36+
37+
year_plot_df = (
38+
source_year_df.groupby("year", as_index=False)
39+
.agg({"item": list})
40+
.sort_values("year")
41+
)
42+
43+
year_plot_df["items_str"] = year_plot_df["item"].apply(lambda items: "<br>".join(items))
44+
year_plot_df["yearly_count"] = year_plot_df["item"].apply(len)
45+
46+
year_plot_df["yearly_cumulative_count"] = year_plot_df["yearly_count"].cumsum()
47+
48+
fig = px.line(
49+
year_plot_df,
50+
x="year",
51+
y="yearly_cumulative_count",
52+
markers=True,
53+
title=source_name,
54+
labels={"year": "Year", "yearly_cumulative_count": "Cumulative Items"},
55+
custom_data=["items_str", "yearly_count"],
56+
template="simple_white",
57+
)
58+
59+
fig.update_traces(
60+
marker={"size": 7, "color": line_color},
61+
line={"color": line_color},
62+
hovertemplate=(
63+
f"Year: %{{x}}<br>"
64+
f"{yearly_label}: %{{customdata[1]}}<br>"
65+
f"{cumulative_label}: %{{y}}<extra></extra>"
66+
),
67+
)
68+
69+
fig.update_layout(
70+
showlegend=False,
71+
height=430,
72+
margin={"l": 40, "r": 20, "t": 60, "b": 50},
73+
yaxis_title="Cumulative Items",
74+
)
75+
fig.update_xaxes(title_text="Year")
76+
return fig
77+
78+
79+
def get_combined_layout(github_df, epmc_entries_df, pypi_first_releases_df, epmc_citations):
80+
"""Build top-level combined chart layout for all three data sources."""
81+
gh_df = github_df.copy() if github_df is not None else pd.DataFrame()
82+
ep_df = epmc_entries_df.copy() if epmc_entries_df is not None else pd.DataFrame()
83+
py_df = pypi_first_releases_df.copy() if pypi_first_releases_df is not None else pd.DataFrame()
84+
ct_df = pd.DataFrame(
85+
epmc_citations.get("citations_over_years", [])
86+
if isinstance(epmc_citations, dict)
87+
else epmc_citations if isinstance(epmc_citations, list)
88+
else []
89+
).reindex(columns=["pub_year", "year_count"])
90+
91+
if not gh_df.empty and "created_on" in gh_df.columns:
92+
gh_df["created_on_year"] = pd.to_datetime(gh_df["created_on"], errors="coerce", utc=True).dt.year
93+
94+
if not py_df.empty and "release_date" in py_df.columns:
95+
py_df["release_year"] = pd.to_datetime(py_df["release_date"], errors="coerce", utc=True).dt.year
96+
97+
if not ct_df.empty:
98+
ct_df["pub_year"] = pd.to_numeric(ct_df["pub_year"], errors="coerce")
99+
ct_df["year_count"] = pd.to_numeric(ct_df["year_count"], errors="coerce").fillna(0).astype(int)
100+
ct_df = ct_df[(ct_df["pub_year"] > 2013) & (ct_df["year_count"] > 0)]
101+
ct_df = ct_df.loc[ct_df.index.repeat(ct_df["year_count"])].copy()
102+
ct_df["citation_item"] = "citation-" + ct_df.groupby("pub_year").cumcount().add(1).astype(str)
103+
104+
github_year_df = _build_source_year_df(gh_df, "created_on_year", "name", "GitHub Repositories")
105+
epmc_year_df = _build_source_year_df(ep_df, "pub_year", "title", "GA4GH-Related Articles")
106+
pypi_year_df = _build_source_year_df(py_df, "release_year", "project_name", "PyPI Packages")
107+
citations_year_df = _build_source_year_df(ct_df, "pub_year", "citation_item", "Europe PMC Cumulative Citations")
108+
109+
gh_fig = _make_source_growth_figure(
110+
github_year_df, "GitHub Repositories", "#1b75bb"
111+
)
112+
113+
epmc_fig = _make_source_growth_figure(
114+
epmc_year_df, "GA4GH-Related Articles", "#e34a3a",
115+
yearly_label="New articles",
116+
cumulative_label="Total articles to date",
117+
)
118+
pypi_fig = _make_source_growth_figure(
119+
pypi_year_df, "PyPI Packages", "#9f79b0",
120+
yearly_label="New libraries created",
121+
cumulative_label="Total libraries to date",
122+
)
123+
124+
citations_fig = _make_source_growth_figure(
125+
citations_year_df,
126+
"Europe PMC Cumulative Citations Per Year",
127+
"#8cc63e",
128+
yearly_label="New citations",
129+
cumulative_label="Total citations to date",
130+
)
131+
132+
gh_fig.update_layout(yaxis_title="Cumulative Repositories")
133+
epmc_fig.update_layout(yaxis_title="Cumulative Articles")
134+
pypi_fig.update_layout(yaxis_title="Cumulative Libraries")
135+
citations_fig.update_layout(yaxis_title="Cumulative Citations")
136+
137+
138+
return dbc.Card(
139+
dbc.CardBody(
140+
html.Figure([
141+
dbc.Row(
142+
[
143+
dbc.Col(dcc.Graph(id="combined-growth-epmc", figure=epmc_fig), lg=6, md=6, sm=12),
144+
dbc.Col(dcc.Graph(id="combined-citations-over-years", figure=citations_fig), lg=6, md=6, sm=12),
145+
dbc.Col(dcc.Graph(id="combined-growth-github", figure=gh_fig), lg=6, md=6, sm=12),
146+
dbc.Col(dcc.Graph(id="combined-growth-pypi", figure=pypi_fig), lg=6, md=6, sm=12),
147+
],
148+
className="g-3",
149+
),
150+
html.Figcaption("Cumulative number of GA4GH-Related Articles and their Citations from Europe PMC, as well as GitHub Repositories, and PyPI Packages per year.")
151+
])
152+
),
153+
className="mb-4 shadow-sm",
154+
style={"borderRadius": "12px"},
155+
)

0 commit comments

Comments
 (0)