Skip to content

Commit 1c74558

Browse files
Merge pull request #25 from ga4gh/dc-custom-desgin-changes
Dc custom desgin changes
2 parents 49e33f2 + 0158344 commit 1c74558

4 files changed

Lines changed: 252 additions & 67 deletions

File tree

app/callbacks/epmc_callbacks.py

Lines changed: 122 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from dash import Input, Output, State
1+
from dash import Input, Output, State, ctx, no_update
22
import dash_bootstrap_components as dbc
33
from dash import html, dcc
44
import pandas as pd
@@ -10,8 +10,13 @@
1010
from app.constants.constants import COUNTRIES_WHITELIST
1111

1212

13-
def fig_epmc_countries_pie(countries_df):
14-
"""Pie chart – article count by affiliation country."""
13+
def fig_epmc_countries_pie(countries_df, hidden_labels=None):
14+
"""Pie chart – article count by affiliation country.
15+
16+
hidden_labels: collection of country names currently toggled off in the
17+
legend. Percentages are recalculated against the visible-only total so
18+
the displayed values stay correct after toggling.
19+
"""
1520
if countries_df is None or countries_df.empty:
1621
return go.Figure().update_layout(title="No country data available")
1722

@@ -34,24 +39,32 @@ def fig_epmc_countries_pie(countries_df):
3439
return go.Figure().update_layout(title="No country data available (after filtering)")
3540

3641
counts = pd.to_numeric(df["count"], errors="coerce").fillna(0.0)
37-
total = counts.sum()
38-
if total <= 0:
39-
return go.Figure().update_layout(title="No country data available (zero total)")
40-
4142
df = df.copy()
4243
df["count"] = counts
4344
df = df.sort_values("count", ascending=False).reset_index(drop=True)
4445

45-
percents = (df["count"] / total * 100)
46+
hidden = set(hidden_labels) if hidden_labels else set()
47+
visible_mask = ~df["country_normalized"].isin(hidden)
48+
visible_total = df.loc[visible_mask, "count"].sum()
49+
if visible_total <= 0:
50+
visible_total = df["count"].sum()
51+
if visible_total <= 0:
52+
return go.Figure().update_layout(title="No country data available (zero total)")
53+
4654
slice_text = []
4755
hover_text = []
48-
for cn, cnt, pct in zip(df["country_normalized"], df["count"], percents):
49-
pct_fmt = f"{pct:.1f}%"
50-
if pct > 5.0:
51-
slice_text.append(f"{cn}<br>{pct_fmt}")
56+
for cn, cnt, is_vis in zip(df["country_normalized"], df["count"], visible_mask):
57+
if not is_vis:
58+
slice_text.append("")
59+
hover_text.append("")
5260
else:
53-
slice_text.append(f"{pct_fmt}")
54-
hover_text.append(f"{cn}: {int(cnt)} ({pct_fmt})")
61+
pct = cnt / visible_total * 100
62+
pct_fmt = f"{pct:.1f}%"
63+
if pct > 5.0:
64+
slice_text.append(f"{cn}<br>{pct_fmt}")
65+
else:
66+
slice_text.append(pct_fmt)
67+
hover_text.append(f"{cn}: {int(cnt)} ({pct_fmt})")
5568

5669
text_positions = ["outside" if "<br>" in t else "inside" for t in slice_text]
5770

@@ -84,6 +97,72 @@ def fig_epmc_countries_pie(countries_df):
8497
x=0.5,
8598
),
8699
)
100+
if hidden:
101+
fig.update_layout(hiddenlabels=list(hidden))
102+
return fig
103+
104+
105+
def fig_epmc_countries_choropleth(countries_df):
106+
"""Choropleth world map — each country's % share of total author affiliations."""
107+
if countries_df is None or countries_df.empty:
108+
return go.Figure().update_layout(title="No country data available")
109+
110+
cols = list(countries_df.columns)
111+
if "country" in [c.lower() for c in cols] and "count" in [c.lower() for c in cols]:
112+
country_col = next(c for c in cols if c.lower() == "country")
113+
count_col = next(c for c in cols if c.lower() == "count")
114+
df = countries_df[[country_col, count_col]].copy()
115+
df.columns = ["country", "count"]
116+
else:
117+
df = countries_df.iloc[:, :2].copy()
118+
df.columns = ["country", "count"]
119+
120+
whitelist = {c.strip().lower() for c in COUNTRIES_WHITELIST}
121+
df["country"] = df["country"].astype(str).str.strip()
122+
df = df[df["country"].str.lower().isin(whitelist)].copy()
123+
df["count"] = pd.to_numeric(df["count"], errors="coerce").fillna(0)
124+
125+
if df.empty:
126+
return go.Figure().update_layout(title="No country data available")
127+
128+
total = df["count"].sum()
129+
df["pct"] = (df["count"] / total * 100).round(2)
130+
df["hover_text"] = df.apply(
131+
lambda r: f"{r['country']}<br>{r['pct']}% of author affiliations", axis=1
132+
)
133+
134+
fig = px.choropleth(
135+
df,
136+
locations="country",
137+
locationmode="country names",
138+
color="pct",
139+
color_continuous_scale="Reds",
140+
custom_data=["pct"],
141+
labels={"pct": "Share (%)", "country": "Country"},
142+
template="simple_white",
143+
)
144+
fig.update_traces(
145+
hovertemplate="<b>%{location}</b><br>%{customdata[0]:.2f}% of author affiliations<extra></extra>",
146+
marker_line_color="white",
147+
marker_line_width=0.5,
148+
)
149+
fig.update_layout(
150+
autosize=True,
151+
margin={"l": 0, "r": 0, "t": 0, "b": 0},
152+
coloraxis_colorbar={
153+
"title": "Share (%)",
154+
"thickness": 12,
155+
"ticksuffix": "%",
156+
},
157+
)
158+
fig.update_geos(
159+
showland=True, landcolor="#DAECC1",
160+
showocean=True, oceancolor="#BBDFF1",
161+
showlakes=True, lakecolor="#BBDFF1",
162+
showcountries=True, countrycolor="#999999",
163+
projection_type="natural earth",
164+
showframe=False,
165+
)
87166
return fig
88167

89168

@@ -424,11 +503,18 @@ def aff_item(num, org):
424503
Output("epmc-countries-pie", "figure"),
425504
Output("epmc-authors-bar", "figure"),
426505
Output("epmc-authors-card-body", "style"),
427-
Input("epmc-top-n-slider", "value"), # Responds to slider but uses same cached authors
506+
Input("epmc-top-n-slider", "value"),
507+
Input("epmc-countries-pie", "relayoutData"),
428508
)
429-
def update_epmc_graphs(top_n):
509+
def update_epmc_graphs(top_n, relayout_data):
510+
# Legend toggle on the pie — only rebuild the pie with updated percentages
511+
if ctx.triggered_id == "epmc-countries-pie":
512+
hidden = (relayout_data or {}).get("hiddenlabels") or []
513+
if "hiddenlabels" not in (relayout_data or {}):
514+
return no_update, no_update, no_update
515+
return fig_epmc_countries_pie(countries_df, hidden_labels=hidden), no_update, no_update
516+
430517
fig_pie = fig_epmc_countries_pie(countries_df)
431-
# Use pre-fetched top_authors_default (no API call needed)
432518
fig_bar = fig_epmc_top_authors_bar(top_authors_default, top_n)
433519
graph_height = max(400, 25 * min(top_n, len(top_authors_default)))
434520
return fig_pie, fig_bar, {"minHeight": f"{graph_height + 96}px"}
@@ -463,3 +549,22 @@ def toggle_aff_collapse(n, is_open, first_affiliation):
463549
html.Span(first_affiliation or "Affiliations"),
464550
]
465551
return new_state, label
552+
553+
# -----------------------
554+
# Interactive YoY growth KPI
555+
# -----------------------
556+
@app.callback(
557+
Output("yoy-growth-value", "children"),
558+
Input("yoy-year-selector", "value"),
559+
State("yearly-pub-counts", "data"),
560+
)
561+
def update_yoy_growth(selected_year, yearly_counts):
562+
if not selected_year or not yearly_counts:
563+
return "N/A"
564+
curr = yearly_counts.get(str(selected_year)) or yearly_counts.get(selected_year)
565+
prev = yearly_counts.get(str(selected_year - 1)) or yearly_counts.get(selected_year - 1)
566+
if curr is None or prev is None or prev == 0:
567+
return "N/A"
568+
pct = round((curr - prev) / prev * 100, 1)
569+
return f"+{pct}%" if pct >= 0 else f"{pct}%"
570+

app/callbacks/persona_callbacks.py

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -47,14 +47,19 @@
4747

4848
# dbc.Col KPI cards — shown with {} (let Bootstrap flex handle sizing)
4949
ALL_COL_IDS = [
50-
# Default KPIs — EPMC-derived; hidden for Developer persona
50+
# EPMC KPIs
51+
"kpi-publications",
5152
"kpi-authors",
5253
"kpi-citations",
5354
"kpi-countries",
54-
# Persona-specific KPIs
55+
# GitHub / PyPI KPIs — now persona-controlled
56+
"kpi-github",
57+
"kpi-pypi",
58+
# Funder / researcher shared KPIs
5559
"funder-kpi-yoy",
5660
"funder-kpi-avg-citations",
57-
"funder-kpi-funding-bodies",
61+
# Researcher-only KPI
62+
"researcher-kpi-open-access",
5863
]
5964

6065
ALL_CONTROLLED_IDS = ALL_SECTION_IDS + ALL_COL_IDS
@@ -66,28 +71,32 @@
6671
PERSONA_SHOW = {
6772
"default": {
6873
"sections": ["servicemap", "metrics", "epmc", "github", "pypi", "tables"],
69-
"cols": ["kpi-authors", "kpi-citations", "kpi-countries"],
74+
"cols": ["kpi-publications", "kpi-authors", "kpi-citations", "kpi-countries",
75+
"kpi-github", "kpi-pypi"],
7076
},
7177
"funder": {
7278
"sections": ["metrics", "epmc", "publication-charts", "funder-only-charts"],
73-
"cols": ["kpi-authors", "kpi-citations", "kpi-countries",
74-
"funder-kpi-yoy", "funder-kpi-avg-citations", "funder-kpi-funding-bodies"],
79+
"cols": ["kpi-publications", "kpi-authors", "kpi-citations", "kpi-countries",
80+
"funder-kpi-yoy", "funder-kpi-avg-citations"],
7581
},
7682
"researcher": {
7783
"sections": ["metrics", "epmc", "tables", "publication-charts", "researcher-charts"],
78-
"cols": ["kpi-authors", "kpi-citations", "kpi-countries",
79-
"funder-kpi-yoy", "funder-kpi-avg-citations"],
84+
"cols": ["kpi-publications", "kpi-authors", "kpi-citations", "kpi-countries",
85+
"funder-kpi-yoy", "funder-kpi-avg-citations",
86+
"researcher-kpi-open-access"],
8087
},
8188
"developer": {
8289
"sections": ["servicemap", "metrics", "github", "pypi", "developer-charts"],
83-
"cols": [], # EPMC KPIs hidden — not relevant for developer view
90+
"cols": ["kpi-github", "kpi-pypi"],
8491
},
8592
"community": {
8693
"sections": ["servicemap", "metrics", "epmc", "github", "pypi", "tables",
8794
"publication-charts", "funder-only-charts", "researcher-charts",
8895
"developer-charts", "community-charts"],
89-
"cols": ["kpi-authors", "kpi-citations", "kpi-countries",
90-
"funder-kpi-yoy", "funder-kpi-avg-citations", "funder-kpi-funding-bodies"],
96+
"cols": ["kpi-publications", "kpi-authors", "kpi-citations", "kpi-countries",
97+
"kpi-github", "kpi-pypi",
98+
"funder-kpi-yoy", "funder-kpi-avg-citations",
99+
"researcher-kpi-open-access"],
91100
},
92101
}
93102

app/layouts/funder_layout.py

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -144,16 +144,18 @@ def _kpi_card(value, label, color_class):
144144
# Public layout builders
145145
# ---------------------------------------------------------------------------
146146

147-
def get_publication_charts_section(entries_df):
147+
def get_publication_charts_section(entries_df, choropleth_fig=None):
148148
"""
149-
Annual publications bar — shared across personas (funder + researcher).
150-
Hidden by default; persona callback sets display:block.
149+
Annual publications bar + global author distribution choropleth.
150+
Shared across funder + researcher personas; hidden by default.
151151
"""
152152
annual_fig = _annual_publications_figure(entries_df)
153153

154154
return html.Div(
155155
[
156156
html.Div("Publication Trends", className="section-title"),
157+
158+
# Row 1: annual bar chart
157159
dbc.Row(
158160
dbc.Col(
159161
dbc.Card(
@@ -175,6 +177,33 @@ def get_publication_charts_section(entries_df):
175177
),
176178
width=12,
177179
),
180+
className="mb-2",
181+
),
182+
183+
# Row 2: full-width choropleth
184+
dbc.Row(
185+
dbc.Col(
186+
dbc.Card(
187+
dbc.CardBody(
188+
html.Figure([
189+
html.H5("Global Author Affiliation Distribution", style={"marginBottom": "6px"}),
190+
dcc.Graph(
191+
id="epmc-countries-choropleth",
192+
figure=choropleth_fig or go.Figure(),
193+
config={"displayModeBar": False},
194+
style={"height": "650px"},
195+
),
196+
html.Figcaption(
197+
"Each country's share (%) of total author affiliations across all GA4GH-related publications. Hover over a country to see its exact percentage.",
198+
style={"fontSize": "13px", "color": "#777", "marginTop": "6px"},
199+
),
200+
])
201+
),
202+
className="mb-4 shadow-sm",
203+
style={"borderRadius": "12px"},
204+
),
205+
width=12,
206+
),
178207
),
179208
],
180209
id="publication-charts",

0 commit comments

Comments
 (0)