Skip to content

Commit ded1cef

Browse files
authored
Merge pull request #10 from ga4gh/cc-epmc-affiliations
Use new affiliations api for EPMC datatable
2 parents 5de8be1 + d9831da commit ded1cef

4 files changed

Lines changed: 115 additions & 66 deletions

File tree

app/callbacks/epmc_callbacks.py

Lines changed: 72 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
import plotly.express as px
88
import plotly.graph_objects as go
99

10-
from app.services.epmc_client import prepare_epmc_data, get_authors_by_article
10+
from app.services.epmc_client import prepare_epmc_data, get_affiliations_by_article
1111
from app.constants.constants import COUNTRIES_WHITELIST
1212

1313

@@ -242,11 +242,11 @@ def update_most_cited_table(_top_n):
242242
)
243243
def show_epmc_details(selected_rows, search_value, year_filter, affiliation_filter):
244244
if not selected_rows or entries_df.empty:
245-
return dbc.Alert("Select an entry to see details", color="info"), None
245+
return dbc.Alert("Select an entry to see details", color="info"), None, None
246246

247247
filtered_df = get_filtered_sorted_df(search_value, year_filter, affiliation_filter)
248248
if filtered_df.empty or selected_rows[0] >= len(filtered_df):
249-
return dbc.Alert("Select an entry to see details", color="info"), None
249+
return dbc.Alert("Select an entry to see details", color="info"), None, None
250250

251251
entry = filtered_df.iloc[selected_rows[0]]
252252

@@ -269,72 +269,79 @@ def show_epmc_details(selected_rows, search_value, year_filter, affiliation_filt
269269
or parsed.get("article_id")
270270
or parsed.get("id")
271271
)
272-
authors = get_authors_by_article(pm_id) if pm_id else []
273-
valid_authors = [a for a in authors if isinstance(a, dict)] if isinstance(authors, list) else []
274-
if valid_authors and any("author_order" in a for a in valid_authors):
275-
valid_authors = sorted(
276-
valid_authors,
277-
key=lambda a: (a.get("author_order") is None, a.get("author_order") or 0),
278-
)
272+
affiliation_rows = get_affiliations_by_article(pm_id) if pm_id else []
273+
affiliation_rows = [r for r in affiliation_rows if isinstance(r, dict)]
274+
affiliation_rows = sorted(
275+
affiliation_rows,
276+
key=lambda r: (
277+
r.get("author_order") is None,
278+
r.get("author_order") or 0,
279+
r.get("affiliation_order") is None,
280+
r.get("affiliation_order") or 0,
281+
),
282+
)
279283

280-
# Build affiliation index: org_name -> number, and reverse map: number -> list of author names
281-
raw_affiliations = parsed.get("affiliations") or []
282-
affiliation_index = {} # org_name -> aff_number
283-
affiliation_list = [] # list of (number, org_name)
284-
aff_counter = 1
285-
286-
# First pass: build affiliation index
287-
if isinstance(raw_affiliations, list):
288-
for aff in sorted(raw_affiliations, key=lambda x: x.get("affiliation_order", 0)):
289-
org = aff.get("org_name", "").strip()
290-
if org and org not in affiliation_index:
291-
affiliation_index[org] = aff_counter
292-
affiliation_list.append((aff_counter, org))
293-
aff_counter += 1
294-
295-
# Build affiliation -> author names map
284+
# Build ordered author map from affiliation rows
285+
author_order = []
296286
author_id_to_name = {}
297-
for a in valid_authors:
298-
first = (a.get("firstname") or "").strip()
299-
last = (a.get("lastname") or "").strip()
300-
full = f"{first} {last}".strip() or (a.get("fullname") or "").strip()
301-
if full:
302-
author_id_to_name[a.get("id")] = full
303-
304-
aff_to_authors = {} # aff_number -> list of author names
305-
if isinstance(raw_affiliations, list):
306-
for aff in raw_affiliations:
307-
org = aff.get("org_name", "").strip()
308-
aid = aff.get("author_id")
309-
if org in affiliation_index and aid in author_id_to_name:
310-
aff_num = affiliation_index[org]
311-
if aff_num not in aff_to_authors:
312-
aff_to_authors[aff_num] = []
313-
author_name = author_id_to_name[aid]
314-
if author_name not in aff_to_authors[aff_num]:
315-
aff_to_authors[aff_num].append(author_name)
316-
317-
# Build author items with superscript aff numbers
318-
author_id_to_affs = {} # author_id -> list of aff numbers
319-
if isinstance(raw_affiliations, list):
320-
for aff in raw_affiliations:
321-
org = aff.get("org_name", "").strip()
322-
aid = aff.get("author_id")
323-
if org in affiliation_index and aid:
324-
if aid not in author_id_to_affs:
325-
author_id_to_affs[aid] = []
326-
aff_num = affiliation_index[org]
327-
if aff_num not in author_id_to_affs[aid]:
328-
author_id_to_affs[aid].append(aff_num)
287+
for row in affiliation_rows:
288+
aid = row.get("author_id")
289+
if aid is None:
290+
continue
291+
first = (row.get("firstname") or "").strip()
292+
last = (row.get("lastname") or "").strip()
293+
full = f"{first} {last}".strip() or (row.get("fullname") or "").strip()
294+
if not full:
295+
continue
296+
if aid not in author_id_to_name:
297+
author_id_to_name[aid] = full
298+
author_order.append(aid)
299+
300+
# Build affiliation index and reverse map in affiliation order
301+
affiliation_index = {}
302+
affiliation_list = []
303+
for row in sorted(
304+
affiliation_rows,
305+
key=lambda r: (
306+
r.get("affiliation_order") is None,
307+
r.get("affiliation_order") or 0,
308+
r.get("author_order") is None,
309+
r.get("author_order") or 0,
310+
),
311+
):
312+
org = (row.get("org_name") or "").strip()
313+
if org and org not in affiliation_index:
314+
aff_num = len(affiliation_index) + 1
315+
affiliation_index[org] = aff_num
316+
affiliation_list.append((aff_num, org))
317+
318+
aff_to_authors = {}
319+
author_id_to_affs = {}
320+
for row in affiliation_rows:
321+
aid = row.get("author_id")
322+
org = (row.get("org_name") or "").strip()
323+
if aid is None or org not in affiliation_index or aid not in author_id_to_name:
324+
continue
325+
326+
aff_num = affiliation_index[org]
327+
author_name = author_id_to_name[aid]
328+
329+
if aff_num not in aff_to_authors:
330+
aff_to_authors[aff_num] = []
331+
if author_name not in aff_to_authors[aff_num]:
332+
aff_to_authors[aff_num].append(author_name)
333+
334+
if aid not in author_id_to_affs:
335+
author_id_to_affs[aid] = []
336+
if aff_num not in author_id_to_affs[aid]:
337+
author_id_to_affs[aid].append(aff_num)
329338

330339
author_items = []
331-
for a in valid_authors:
332-
first = (a.get("firstname") or "").strip()
333-
last = (a.get("lastname") or "").strip()
334-
full = f"{first} {last}".strip() or (a.get("fullname") or "").strip()
340+
for aid in author_order:
341+
full = author_id_to_name.get(aid)
335342
if not full:
336343
continue
337-
aff_nums = author_id_to_affs.get(a.get("id"), [])
344+
aff_nums = author_id_to_affs.get(aid, [])
338345
superscript = (" " + ",".join(f"[{n}]" for n in sorted(aff_nums))) if aff_nums else ""
339346
author_items.append(f"{full}{superscript}")
340347

@@ -351,6 +358,7 @@ def aff_item(num, org):
351358
], style={"marginBottom": "6px"})
352359

353360
first_aff_component = aff_item(*affiliation_list[0]) if affiliation_list else html.P("N/A")
361+
first_aff_text = f"{affiliation_list[0][0]}. {affiliation_list[0][1]}" if affiliation_list else "N/A"
354362
rest_aff_components = [aff_item(num, org) for num, org in affiliation_list[1:]] if len(affiliation_list) > 1 else []
355363

356364
# Abstract HTML to Markdown conversion
@@ -434,7 +442,7 @@ def aff_item(num, org):
434442
]),
435443
], style={"boxShadow": "0 4px 10px rgba(0,0,0,0.1)"})
436444

437-
return card, first_author_store, first_aff_component
445+
return card, first_author_store, first_aff_text
438446

439447
# -----------------------
440448
# Build initial charts from cached data

app/callbacks/github_callbacks.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,8 @@ def fig_github_activity_bar(gh_activity_df, color_map=None):
9090
hovertemplate=(
9191
f"Repo: {row['name']}<br>"
9292
f"Activity Score: {row['activity_score']:.4f}<br>"
93+
f"Pushed At: {row['pushed_at_str']}<br>"
94+
f"Last Updated: {row['last_updated_str']}<br>"
9395
"<extra></extra>"
9496
),
9597
)

app/constants/api.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,4 +31,5 @@
3131
EPMC_TOP_AUTHORS = BASE_API + "/epmc/top-authors"
3232
EPMC_ALL_ARTICLES = BASE_API + "/epmc/all-articles"
3333
EPMC_GET_AUTHORS_BY_ARTICLE = BASE_API + "/epmc/get-authors-by-article-id/"
34-
EPMC_CITATION_OVER_YEARS = BASE_API + "/epmc/citations-over-years"
34+
EPMC_CITATION_OVER_YEARS = BASE_API + "/epmc/citations-over-years"
35+
EPMC_AFFILIATION_BY_ARTICLE = BASE_API + "/epmc/get-affiliations-by-article-id/"

app/services/epmc_client.py

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,28 @@ def get_authors_by_article(pm_id):
162162
return []
163163

164164

165+
def get_affiliations_by_article(pm_id):
166+
"""
167+
Fetch affiliation rows for a specific article by PM id using the configured API endpoint.
168+
Returns a list of affiliation/author rows (may be empty).
169+
"""
170+
if not pm_id:
171+
return []
172+
try:
173+
endpoint = api_constants.EPMC_AFFILIATION_BY_ARTICLE + str(pm_id)
174+
data = get_json(endpoint)
175+
if isinstance(data, list):
176+
return data
177+
if isinstance(data, dict):
178+
if "results" in data and isinstance(data["results"], list):
179+
return data["results"]
180+
if "items" in data and isinstance(data["items"], list):
181+
return data["items"]
182+
return []
183+
except Exception:
184+
return []
185+
186+
165187

166188

167189

@@ -171,6 +193,19 @@ def get_authors_by_article(pm_id):
171193

172194
_epmc_cache = {}
173195

196+
197+
def _normalize_pub_year(value):
198+
"""Return a 4-digit publication year as int, or None when invalid."""
199+
if value is None:
200+
return None
201+
if isinstance(value, bool):
202+
return None
203+
try:
204+
year = int(str(value).strip())
205+
except (TypeError, ValueError):
206+
return None
207+
return year if 1000 <= year <= 9999 else None
208+
174209
def prepare_epmc_data():
175210
"""
176211
Fetch and process all EPMC data in a single pass to avoid redundant API calls.
@@ -203,14 +238,17 @@ def prepare_epmc_data():
203238
if isinstance(raw_entries, list):
204239
sanitized = []
205240
for e in raw_entries:
241+
pub_year = _normalize_pub_year(e.get("pub_year") or e.get("year"))
206242
record = {
207243
"title": e.get("title") or "",
208244
"doi": e.get("doi") or "",
209-
"pub_year": e.get("pub_year") or e.get("year") or "",
245+
"pub_year": pub_year,
210246
"raw_json": json.dumps(e, ensure_ascii=False),
211247
}
212248
sanitized.append(record)
213249
entries_df = pd.DataFrame.from_records(sanitized) if sanitized else pd.DataFrame()
250+
if not entries_df.empty and "pub_year" in entries_df.columns:
251+
entries_df["pub_year"] = pd.array(entries_df["pub_year"], dtype="Int64")
214252

215253
# Build countries DataFrame
216254
if isinstance(raw_countries, dict):

0 commit comments

Comments
 (0)