OSCR

Beyond bacteria: a multi-omics view of the gut–brain axis in Parkinson’s disease

Code ↔ Paper

4 matches between paragraphs of the paper and lines of its authors' code, computed by the harvester (lexical-v1). Click a colored paragraph or line to see its counterpart.

The 4 matches
  1. [1] § Materials and methods › Study selection and eligibility criteria ↔ app.R, lines 1413–1461 · score 0.68 · conference abstracts, technical filters, English, human, publications
  2. [2] § Materials and methods › Study selection and eligibility criteria › Inclusion and exclusion criteria for virome, mycobiome, and proteome studies ↔ app.R, lines 1531–1581 · score 0.60 · duplicate removal, flow diagram, technical filtering, database, eligibility, PRISMA
  3. [3] § Materials and methods › Data extraction and quality assessment ↔ Visualization.Rmd, lines 178–209 · score 0.56 · NOS scores, Quality assessment
  4. [4] § Materials and methods › Data extraction and quality assessment ↔ Visualization.Rmd, lines 178–209 · score 0.55 · NOS scores, quality assessment

Paper

Loaded from Europe PMC by your browser, not stored by OSCR: doi.org · Europe PMC

The paper is loaded when this pane is shown.

The authors' code

R · 1,901 lines · 80 KB · no license · 2 matches

  1. # =============================================================================
  2. # PRISMA 2020 Flow Diagram – Multi-Subtopic Publication-Ready Shiny App
  3. #
  4. # Modification: Supports 1–6 independent subtopics, each with its own
  5. # PRISMA flow diagram, databases, technical filters, deduplication,
  6. # and scientific / eligibility criteria.
  7. #
  8. # Diagram layout v3: Publication-grade Graphviz with coloured section
  9. # clusters (IDENTIFICATION / SCREENING / INCLUDED), right-side exclusion
  10. # boxes, and PRISMA-standard palette — closely matching the style seen in
  11. # peer-reviewed systematic reviews.
  12. #
  13. # Compliant with:
  14. # Page MJ, et al. (2021). The PRISMA 2020 statement: an updated guideline
  15. # for reporting systematic reviews. BMJ, 372, n71.
  16. # https://doi.org/10.1136/bmj.n71
  17. #
  18. # Run: shiny::runApp()
  19. # Requires: shiny, DiagrammeR
  20. # DiagrammeRsvg, rsvg (for SVG / PNG downloads — install once)
  21. # =============================================================================
  22. library(shiny)
  23. library(DiagrammeR)
  24. # SVG / PNG export – install once if missing:
  25. if (!requireNamespace("DiagrammeRsvg", quietly = TRUE) ||
  26. !requireNamespace("rsvg", quietly = TRUE)) {
  27. message(
  28. "\n[PRISMA app] SVG / PNG download requires two extra packages.\n",
  29. "Run in R: install.packages(c('DiagrammeRsvg', 'rsvg'))\n",
  30. "DOT and CSV downloads still work without them.\n"
  31. )
  32. }
  33. HAS_SVG_EXPORT <- requireNamespace("DiagrammeRsvg", quietly = TRUE) &&
  34. requireNamespace("rsvg", quietly = TRUE)
  35. APP_VERSION <- "4.2.0"
  36. APP_DATE <- "2025"
  37. MAX_ST <- 6 # Maximum number of subtopics supported
  38. # =============================================================================
  39. # SECTION 1 – UTILITY HELPERS
  40. # =============================================================================
  41. `%||%` <- function(a, b) if (is.null(a)) b else a
  42. trim_lines <- function(x) {
  43. x <- x %||% ""
  44. out <- trimws(unlist(strsplit(x, "\n", fixed = TRUE)))
  45. out[nzchar(out)]
  46. }
  47. safe_int <- function(x) {
  48. if (is.null(x)) return(0L)
  49. if (is.list(x)) x <- unlist(x, use.names = FALSE)
  50. if (length(x) == 0) return(0L)
  51. x <- x[[1]]
  52. x <- suppressWarnings(as.integer(x))
  53. if (is.na(x) || x < 0) 0L else x
  54. }
  55. # =============================================================================
  56. # SECTION 2 – GRAPHVIZ / HTML LABEL HELPERS
  57. # =============================================================================
  58. gv_esc <- function(s) {
  59. s <- gsub("&", "&amp;", s)
  60. s <- gsub("<", "&lt;", s)
  61. s <- gsub(">", "&gt;", s)
  62. s
  63. }
  64. make_filter_label_html <- function(title, criteria, mode = c("total", "per"),
  65. per_removed = NULL, total_removed = 0L,
  66. remaining = NA_integer_,
  67. included = NA_integer_,
  68. show_included = FALSE,
  69. cellpadding = 6) {
  70. mode <- match.arg(mode)
  71. criteria <- criteria[!is.na(criteria) & nzchar(criteria)]
  72. if (length(criteria) == 0) criteria <- "(no criteria specified)"
  73. crit_lines <- if (mode == "per") {
  74. per_removed <- per_removed %||% rep(0L, length(criteria))
  75. per_removed <- as.integer(per_removed)
  76. if (length(per_removed) != length(criteria)) per_removed <- rep(0L, length(criteria))
  77. paste0(criteria, " (Removed: ", pmax(0L, per_removed), ")")
  78. } else {
  79. paste0(criteria)
  80. }
  81. removed_line <- paste0("Removed: ", pmax(0L, as.integer(total_removed)))
  82. tail_lines <- c(
  83. paste0("Records identified (n): ", pmax(0L, as.integer(included))),
  84. removed_line,
  85. paste0("Remaining: ", pmax(0L, as.integer(remaining)))
  86. )
  87. rows <- c(
  88. paste0("<TR><TD ALIGN='CENTER'><B>", gv_esc(title), "</B></TD></TR>"),
  89. paste0("<TR><TD ALIGN='CENTER'>", gv_esc(tail_lines), "</TD></TR>", collapse = ""),
  90. paste0("<TR><TD ALIGN='LEFT'>", gv_esc(crit_lines), "</TD></TR>", collapse = "")
  91. )
  92. paste0("<<TABLE BORDER='0' CELLBORDER='0' CELLSPACING='0' CELLPADDING='", cellpadding, "'>",
  93. paste(rows, collapse = ""),
  94. "</TABLE>>")
  95. }
  96. make_simple_box_html <- function(title, lines,
  97. bold_title = TRUE,
  98. title_wrap = FALSE,
  99. cellpadding = 8,
  100. td_width = NULL) {
  101. title_lines <- if (title_wrap) {
  102. w <- 22
  103. if (nchar(title) <= w) {
  104. title
  105. } else {
  106. cut <- regexpr(" [^ ]*$", substr(title, 1, w))
  107. if (cut > 0) {
  108. c(trimws(substr(title, 1, cut)), trimws(substr(title, cut + 1, nchar(title))))
  109. } else {
  110. c(substr(title, 1, w), substr(title, w + 1, nchar(title)))
  111. }
  112. }
  113. } else {
  114. title
  115. }
  116. title_joined <- paste(vapply(title_lines, gv_esc, character(1)), collapse = "<BR/>")
  117. if (bold_title) title_joined <- paste0("<B>", title_joined, "</B>")
  118. td_attr <- if (!is.null(td_width)) paste0(" WIDTH='", td_width, "'") else ""
  119. rows <- c(
  120. paste0("<TR><TD ALIGN='CENTER'", td_attr, ">", title_joined, "</TD></TR>"),
  121. paste0("<TR><TD ALIGN='CENTER'", td_attr, ">", gv_esc(lines), "</TD></TR>", collapse = "")
  122. )
  123. paste0("<<TABLE BORDER='0' CELLBORDER='0' CELLSPACING='0' CELLPADDING='", cellpadding, "'>",
  124. paste(rows, collapse = ""),
  125. "</TABLE>>")
  126. }
  127. # ---------------------------------------------------------------------------
  128. # make_excl_box_html
  129. # Right-side exclusion box: centred bold title + LEFT-aligned bullet rows.
  130. # Used for "Records removed before screening" and "Records excluded".
  131. # ---------------------------------------------------------------------------
  132. make_excl_box_html <- function(title, lines, cellpadding = 10, td_width = NULL) {
  133. lines <- lines[!is.na(lines) & nzchar(lines)]
  134. td_attr <- if (!is.null(td_width)) paste0(" WIDTH='", td_width, "'") else ""
  135. title_row <- paste0("<TR><TD ALIGN='CENTER'", td_attr,
  136. "><B>", gv_esc(title), "</B></TD></TR>")
  137. body_rows <- paste0(
  138. "<TR><TD ALIGN='LEFT'", td_attr, ">",
  139. gv_esc(lines),
  140. "</TD></TR>",
  141. collapse = ""
  142. )
  143. paste0("<<TABLE BORDER='0' CELLBORDER='0' CELLSPACING='2' CELLPADDING='",
  144. cellpadding, "'>",
  145. title_row, body_rows,
  146. "</TABLE>>")
  147. }
  148. # ---------------------------------------------------------------------------
  149. # make_db_id_html
  150. # Compact DB box for the IDENTIFICATION cluster: bold name + n = X.
  151. # ---------------------------------------------------------------------------
  152. make_db_id_html <- function(db_name, n, cellpadding = 10, td_width = 160) {
  153. td_attr <- paste0(" WIDTH='", td_width, "'")
  154. paste0(
  155. "<<TABLE BORDER='0' CELLBORDER='0' CELLSPACING='0' CELLPADDING='",
  156. cellpadding, "'>",
  157. "<TR><TD ALIGN='CENTER'", td_attr, "><B>", gv_esc(db_name), "</B></TD></TR>",
  158. "<TR><TD ALIGN='CENTER'", td_attr, ">n\u2009=\u2009",
  159. format(n, big.mark = ","), "</TD></TR>",
  160. "</TABLE>>"
  161. )
  162. }
  163. # =============================================================================
  164. # SECTION 3 – USER INTERFACE (UI)
  165. # =============================================================================
  166. ui <- fluidPage(
  167. # ---------- HEAD: Google Fonts + publication-grade CSS ----------
  168. tags$head(
  169. tags$link(rel = "preconnect", href = "https://fonts.googleapis.com"),
  170. tags$link(rel = "preconnect", href = "https://fonts.gstatic.com",
  171. crossorigin = NA),
  172. tags$link(rel = "stylesheet",
  173. href = paste0("https://fonts.googleapis.com/css2?",
  174. "family=Lora:ital,wght@0,400;0,600;0,700;1,400&",
  175. "family=Source+Sans+3:wght@300;400;600;700&",
  176. "display=swap")),
  177. tags$style(HTML("
  178. /* ── Global ── */
  179. *, *::before, *::after { box-sizing: border-box; }
  180. body {
  181. font-family: 'Source Sans 3', sans-serif;
  182. font-size: 14px;
  183. background: #F5F6FA;
  184. color: #1a2130;
  185. }
  186. /* ── App title bar ── */
  187. .app-header {
  188. background: linear-gradient(135deg, #0D2B52 0%, #1B4F8C 100%);
  189. color: #fff;
  190. padding: 18px 28px 14px 28px;
  191. margin: -15px -15px 20px -15px;
  192. border-bottom: 3px solid #C49A2A;
  193. }
  194. .app-header h2 {
  195. font-family: 'Lora', serif;
  196. font-size: 20px;
  197. font-weight: 700;
  198. margin: 0 0 4px 0;
  199. letter-spacing: 0.2px;
  200. }
  201. .app-header .app-subtitle {
  202. font-size: 12px;
  203. font-weight: 300;
  204. opacity: 0.82;
  205. letter-spacing: 0.3px;
  206. }
  207. .app-header .prisma-badge {
  208. display: inline-block;
  209. background: #C49A2A;
  210. color: #fff;
  211. font-size: 10px;
  212. font-weight: 700;
  213. letter-spacing: 1px;
  214. padding: 2px 8px;
  215. border-radius: 3px;
  216. margin-left: 10px;
  217. vertical-align: middle;
  218. }
  219. .app-header .multi-badge {
  220. display: inline-block;
  221. background: rgba(255,255,255,0.18);
  222. color: #fff;
  223. font-size: 10px;
  224. font-weight: 600;
  225. letter-spacing: 0.8px;
  226. padding: 2px 8px;
  227. border-radius: 3px;
  228. margin-left: 6px;
  229. vertical-align: middle;
  230. border: 1px solid rgba(255,255,255,0.35);
  231. }
  232. /* ── Sidebar ── */
  233. .well {
  234. background: #F5F6FA !important;
  235. border: none !important;
  236. box-shadow: none !important;
  237. padding: 0 !important;
  238. }
  239. .sidebar-scroll {
  240. height: calc(100vh - 145px);
  241. overflow-y: auto;
  242. padding-right: 4px;
  243. }
  244. .sidebar-scroll::-webkit-scrollbar { width: 5px; }
  245. .sidebar-scroll::-webkit-scrollbar-thumb {
  246. background: #c5ccd9; border-radius: 4px;
  247. }
  248. /* ── Collapsible panels ── */
  249. details {
  250. border: 1px solid #dce2ed;
  251. border-radius: 8px;
  252. padding: 0;
  253. background: #fff;
  254. margin-bottom: 10px;
  255. box-shadow: 0 1px 3px rgba(0,0,0,0.05);
  256. overflow: hidden;
  257. }
  258. summary {
  259. cursor: pointer;
  260. font-weight: 700;
  261. font-size: 12.5px;
  262. letter-spacing: 0.4px;
  263. text-transform: uppercase;
  264. color: #0D2B52;
  265. background: #EDF1F8;
  266. padding: 9px 12px;
  267. list-style: none;
  268. border-bottom: 1px solid #dce2ed;
  269. user-select: none;
  270. }
  271. summary::-webkit-details-marker { display: none; }
  272. summary::after { content: '▸'; float: right; color: #6b7280; }
  273. details[open] summary::after { content: '▾'; }
  274. .panel-body { padding: 10px 12px 12px 12px; }
  275. /* ── Subtopic highlight panel (new) ── */
  276. details.subtopic-panel > summary {
  277. background: #E8F0FB;
  278. color: #1B3F7A;
  279. border-left: 3px solid #1B4F8C;
  280. }
  281. .st-active-label {
  282. font-size: 11px;
  283. font-weight: 700;
  284. text-transform: uppercase;
  285. letter-spacing: 0.8px;
  286. color: #1B4F8C;
  287. background: #E8F0FB;
  288. border: 1px solid #b3c8e8;
  289. border-radius: 4px;
  290. padding: 4px 10px;
  291. margin-bottom: 10px;
  292. display: block;
  293. }
  294. /* ── Active-subtopic picker (radio list, always visible) ── */
  295. .active-st-picker {
  296. background: #fff;
  297. border: 1px solid #dce2ed;
  298. border-radius: 6px;
  299. padding: 6px 10px 4px 10px;
  300. margin-top: 4px;
  301. }
  302. .active-st-picker .radio,
  303. .active-st-picker .form-check,
  304. .active-st-picker .shiny-options-group > div {
  305. margin: 4px 0 4px 0;
  306. }
  307. .active-st-picker label,
  308. .active-st-picker .radio label,
  309. .active-st-picker .form-check-label {
  310. font-weight: 600 !important;
  311. color: #0D2B52 !important;
  312. font-size: 13px !important;
  313. cursor: pointer;
  314. }
  315. /* ── Resizable two-pane layout (replaces sidebarLayout) ── */
  316. .app-layout {
  317. display: flex;
  318. align-items: stretch;
  319. margin: 0 -15px; /* pull out to fluidPage container edges */
  320. min-height: calc(100vh - 110px);
  321. }
  322. .sidebar-pane {
  323. flex: 0 0 460px; /* default sidebar width (was ~33% of 12-col grid) */
  324. min-width: 320px;
  325. max-width: 900px;
  326. padding: 0 15px;
  327. background: #F5F6FA;
  328. position: relative;
  329. }
  330. .main-pane {
  331. flex: 1 1 auto;
  332. min-width: 0; /* allows flex item to shrink below content size */
  333. padding: 0 15px;
  334. }
  335. .split-handle {
  336. flex: 0 0 6px;
  337. cursor: col-resize;
  338. background: #dce2ed;
  339. border-left: 1px solid #c5ccd9;
  340. border-right: 1px solid #c5ccd9;
  341. transition: background 0.15s;
  342. position: relative;
  343. }
  344. .split-handle::before {
  345. content: '';
  346. position: absolute;
  347. top: 50%;
  348. left: 50%;
  349. width: 2px;
  350. height: 36px;
  351. margin-left: -1px;
  352. margin-top: -18px;
  353. background: repeating-linear-gradient(
  354. to bottom,
  355. #8a96ad 0,
  356. #8a96ad 3px,
  357. transparent 3px,
  358. transparent 6px
  359. );
  360. border-radius: 1px;
  361. }
  362. .split-handle:hover,
  363. .split-handle.dragging {
  364. background: #1B4F8C;
  365. }
  366. .split-handle:hover::before,
  367. .split-handle.dragging::before {
  368. background: repeating-linear-gradient(
  369. to bottom,
  370. #fff 0,
  371. #fff 3px,
  372. transparent 3px,
  373. transparent 6px
  374. );
  375. }
  376. body.split-dragging {
  377. cursor: col-resize !important;
  378. user-select: none !important;
  379. }
  380. /* ── Card / db-box ── */
  381. .db-box {
  382. padding: 10px 12px;
  383. border: 1px solid #e0e5f0;
  384. border-radius: 7px;
  385. margin-bottom: 9px;
  386. background: #FAFBFD;
  387. }
  388. .db-title {
  389. font-weight: 700;
  390. font-size: 12.5px;
  391. color: #0D2B52;
  392. margin-bottom: 6px;
  393. border-bottom: 1px solid #e8ecf3;
  394. padding-bottom: 4px;
  395. }
  396. /* ── Helper text ── */
  397. .small-note {
  398. color: #6b7a93;
  399. font-size: 11.5px;
  400. line-height: 1.5;
  401. margin-bottom: 6px;
  402. }
  403. /* ── Validation / warning banner ── */
  404. .warn-box {
  405. background: #FFF8E1;
  406. border-left: 4px solid #F9A825;
  407. border-radius: 0 6px 6px 0;
  408. padding: 8px 12px;
  409. margin-bottom: 10px;
  410. font-size: 12.5px;
  411. color: #7a5500;
  412. }
  413. .error-box {
  414. background: #FFEBEE;
  415. border-left: 4px solid #C62828;
  416. border-radius: 0 6px 6px 0;
  417. padding: 8px 12px;
  418. margin-bottom: 10px;
  419. font-size: 12.5px;
  420. color: #7a0000;
  421. }
  422. /* ── Tab panel ── */
  423. .nav-tabs > li > a {
  424. font-family: 'Source Sans 3', sans-serif;
  425. font-size: 13px;
  426. font-weight: 600;
  427. color: #0D2B52;
  428. letter-spacing: 0.2px;
  429. }
  430. .nav-tabs > li.active > a,
  431. .nav-tabs > li.active > a:hover {
  432. color: #1B4F8C;
  433. border-top: 3px solid #C49A2A !important;
  434. }
  435. .tab-content {
  436. background: #fff;
  437. border: 1px solid #dde2ed;
  438. border-top: none;
  439. border-radius: 0 0 8px 8px;
  440. padding: 16px;
  441. }
  442. /* ── Download button row ── */
  443. .dl-row {
  444. display: flex;
  445. gap: 8px;
  446. margin-bottom: 12px;
  447. flex-wrap: wrap;
  448. }
  449. .btn-dl {
  450. font-family: 'Source Sans 3', sans-serif;
  451. font-size: 12px;
  452. font-weight: 600;
  453. background: #0D2B52;
  454. color: #fff !important;
  455. border: none;
  456. border-radius: 5px;
  457. padding: 6px 14px;
  458. cursor: pointer;
  459. letter-spacing: 0.3px;
  460. transition: background 0.15s;
  461. }
  462. .btn-dl:hover { background: #1B4F8C; color: #fff !important; }
  463. .btn-dl.gold { background: #C49A2A; }
  464. .btn-dl.gold:hover { background: #a07c1a; }
  465. .btn-dl.green { background: #276749; }
  466. .btn-dl.green:hover { background: #1C4532; }
  467. .btn-dl.teal { background: #2C7A7B; }
  468. .btn-dl.teal:hover { background: #1D5959; }
  469. .btn-dl.maroon { background: #8B2331; }
  470. .btn-dl.maroon:hover { background: #66161F; }
  471. /* ── Methods / caption text area ── */
  472. .methods-box {
  473. font-family: 'Lora', serif;
  474. font-size: 13.5px;
  475. line-height: 1.85;
  476. background: #FAFBFC;
  477. border: 1px solid #dce2ed;
  478. border-radius: 6px;
  479. padding: 16px 20px;
  480. white-space: pre-wrap;
  481. color: #1a2130;
  482. }
  483. .methods-label {
  484. font-family: 'Source Sans 3', sans-serif;
  485. font-weight: 700;
  486. font-size: 11px;
  487. letter-spacing: 1px;
  488. text-transform: uppercase;
  489. color: #6b7a93;
  490. margin-bottom: 6px;
  491. }
  492. /* ── About / citation section ── */
  493. .cite-block {
  494. background: #EDF1F8;
  495. border-left: 4px solid #1B4F8C;
  496. border-radius: 0 6px 6px 0;
  497. padding: 12px 16px;
  498. font-family: 'Lora', serif;
  499. font-size: 13px;
  500. line-height: 1.7;
  501. color: #1a2130;
  502. margin-bottom: 14px;
  503. }
  504. .about-h {
  505. font-family: 'Lora', serif;
  506. font-weight: 700;
  507. font-size: 15px;
  508. color: #0D2B52;
  509. margin: 16px 0 6px 0;
  510. padding-bottom: 4px;
  511. border-bottom: 1px solid #dce2ed;
  512. }
  513. .about-p {
  514. font-size: 13.5px;
  515. line-height: 1.75;
  516. color: #2c3e55;
  517. margin-bottom: 10px;
  518. }
  519. /* ── Counts summary verbatim ── */
  520. pre.shiny-text-output {
  521. font-family: 'Courier New', monospace;
  522. font-size: 13px;
  523. background: #F5F6FA;
  524. border: 1px solid #dce2ed;
  525. border-radius: 6px;
  526. padding: 14px 16px;
  527. color: #1a2130;
  528. }
  529. /* ── PRISMA footer ── */
  530. .prisma-footer {
  531. margin-top: 20px;
  532. padding: 10px 16px;
  533. background: #EDF1F8;
  534. border-radius: 6px;
  535. font-size: 11.5px;
  536. color: #4a5568;
  537. line-height: 1.6;
  538. border: 1px solid #dce2ed;
  539. }
  540. .prisma-footer a { color: #1B4F8C; }
  541. /* ── Shiny input overrides ── */
  542. label { font-weight: 600 !important; font-size: 12.5px !important;
  543. color: #1a2130 !important; }
  544. .form-control { font-size: 13px !important; }
  545. .shiny-input-container { margin-bottom: 8px !important; }
  546. .block { margin-top: 6px; }
  547. ")),
  548. # ----- Draggable splitter behaviour (issue 3) -----
  549. tags$script(HTML("
  550. (function() {
  551. function attachSplitter() {
  552. var handle = document.querySelector('.split-handle');
  553. var sidebar = document.querySelector('.sidebar-pane');
  554. if (!handle || !sidebar || handle.dataset.bound === '1') return;
  555. handle.dataset.bound = '1';
  556. var startX, startW;
  557. function onMove(ev) {
  558. var dx = ev.pageX - startX;
  559. var w = startW + dx;
  560. // clamp to CSS min/max
  561. if (w < 320) w = 320;
  562. if (w > 900) w = 900;
  563. sidebar.style.flex = '0 0 ' + w + 'px';
  564. }
  565. function onUp() {
  566. document.removeEventListener('mousemove', onMove);
  567. document.removeEventListener('mouseup', onUp);
  568. handle.classList.remove('dragging');
  569. document.body.classList.remove('split-dragging');
  570. // notify Shiny so plots re-fit to the new main-pane width
  571. if (window.Shiny && Shiny.setInputValue) {
  572. Shiny.setInputValue('sidebar_width_px',
  573. sidebar.getBoundingClientRect().width,
  574. {priority: 'event'});
  575. }
  576. window.dispatchEvent(new Event('resize'));
  577. }
  578. handle.addEventListener('mousedown', function(ev) {
  579. ev.preventDefault();
  580. startX = ev.pageX;
  581. startW = sidebar.getBoundingClientRect().width;
  582. handle.classList.add('dragging');
  583. document.body.classList.add('split-dragging');
  584. document.addEventListener('mousemove', onMove);
  585. document.addEventListener('mouseup', onUp);
  586. });
  587. // Double-click resets to default width
  588. handle.addEventListener('dblclick', function() {
  589. sidebar.style.flex = '';
  590. window.dispatchEvent(new Event('resize'));
  591. });
  592. }
  593. // Run after Shiny has rendered the DOM
  594. if (document.readyState === 'loading') {
  595. document.addEventListener('DOMContentLoaded', attachSplitter);
  596. } else {
  597. attachSplitter();
  598. }
  599. // Also retry shortly after, in case Shiny's UI is built asynchronously
  600. setTimeout(attachSplitter, 200);
  601. setTimeout(attachSplitter, 800);
  602. })();
  603. "))
  604. ),
  605. # ---------- App header ----------
  606. div(class = "app-header",
  607. h2(HTML('PRISMA 2020 Flow Diagram
  608. <span class="prisma-badge">PRISMA 2020</span>
  609. <span class="multi-badge">MULTI-SUBTOPIC</span>')),
  610. div(class = "app-subtitle",
  611. "Systematic review — one independent flow diagram per subtopic — publication ready &middot; v3 cluster layout")
  612. ),
  613. div(class = "app-layout",
  614. # ======================================================================
  615. # SIDEBAR PANE (resizable)
  616. # ======================================================================
  617. div(class = "sidebar-pane",
  618. div(class = "sidebar-scroll",
  619. # ----------------------------------------------------------------
  620. # NEW: Subtopics Manager
  621. # ----------------------------------------------------------------
  622. tags$details(open = TRUE,
  623. tags$summary("Subtopics"),
  624. div(class = "panel-body",
  625. div(class = "small-note",
  626. "Each subtopic has its own databases, filters, and eligibility ",
  627. "criteria, producing an independent PRISMA flow diagram."),
  628. sliderInput("k_st",
  629. "Number of subtopics (flowcharts):",
  630. min = 1, max = MAX_ST, value = 1, step = 1),
  631. uiOutput("st_name_inputs"),
  632. hr(style = "margin:10px 0 8px 0;"),
  633. div(class = "small-note", "Select subtopic to configure below:"),
  634. uiOutput("st_selector_ui")
  635. )
  636. ),
  637. # ----------------------------------------------------------------
  638. # Study Metadata (shared across all subtopics)
  639. # ----------------------------------------------------------------
  640. tags$details(open = FALSE,
  641. tags$summary("0) Study Metadata"),
  642. div(class = "panel-body",
  643. div(class = "small-note",
  644. "Shared metadata used in auto-generated text for all subtopics."),
  645. textInput("meta_authors", "Author(s)",
  646. placeholder = "e.g. Smith J, Jones A"),
  647. textInput("meta_title", "Review title (short)",
  648. placeholder = "e.g. Gut microbiome and Parkinson's disease"),
  649. textInput("meta_year", "Year",
  650. placeholder = format(Sys.Date(), "%Y")),
  651. textInput("meta_journal", "Target journal",
  652. placeholder = "e.g. Systematic Reviews")
  653. )
  654. ),
  655. # ----------------------------------------------------------------
  656. # Per-subtopic panels 1–4, shown via conditionalPanel
  657. # (conditionalPanel hides/shows with CSS so input values are PRESERVED
  658. # when switching between subtopics)
  659. # ----------------------------------------------------------------
  660. lapply(seq_len(MAX_ST), function(s) {
  661. conditionalPanel(
  662. condition = paste0("input.active_st == '", s,
  663. "' && input.k_st >= ", s),
  664. # Panel 1: Databases
  665. tags$details(open = TRUE, class = "subtopic-panel",
  666. tags$summary("1) Databases"),
  667. div(class = "panel-body",
  668. span(class = "st-active-label",
  669. paste0("Subtopic ", s, " — Databases")),
  670. sliderInput(paste0("k_db_st", s),
  671. "Number of databases searched:",
  672. min = 1, max = 20, value = 4, step = 1),
  673. uiOutput(paste0("db_inputs_st", s)),
  674. uiOutput(paste0("db_val_st", s))
  675. )
  676. ),
  677. # Panel 2: Technical filter
  678. tags$details(open = TRUE, class = "subtopic-panel",
  679. tags$summary("2) Technical filter (per database)"),
  680. div(class = "panel-body",
  681. span(class = "st-active-label",
  682. paste0("Subtopic ", s, " — Technical Filter")),
  683. div(class = "small-note",
  684. "Technical filters are applied per database before merging."),
  685. uiOutput(paste0("tech_ui_st", s)),
  686. uiOutput(paste0("tech_val_st", s))
  687. )
  688. ),
  689. # Panel 3: Deduplication
  690. tags$details(open = FALSE, class = "subtopic-panel",
  691. tags$summary("3) Remove duplicates (merged)"),
  692. div(class = "panel-body",
  693. span(class = "st-active-label",
  694. paste0("Subtopic ", s, " — Deduplication")),
  695. numericInput(paste0("dup_removed_st", s),
  696. "Duplicates removed after merging:",
  697. value = 0, min = 0, step = 1),
  698. uiOutput(paste0("dup_val_st", s))
  699. )
  700. ),
  701. # Panel 4: Scientific / eligibility filter
  702. tags$details(open = FALSE, class = "subtopic-panel",
  703. tags$summary("4) Scientific / eligibility filter"),
  704. div(class = "panel-body",
  705. span(class = "st-active-label",
  706. paste0("Subtopic ", s, " — Eligibility Criteria")),
  707. # --- Optional minimum sample-size filter ---
  708. # (Issue 2) Mirror the "selectable filter" pattern from
  709. # section 2: a checkbox gates whether this criterion is
  710. # applied at all. Some analyses don't filter by n;
  711. # others use n >= 50 per group.
  712. div(class = "db-box",
  713. div(class = "db-title", "Minimum sample size filter"),
  714. checkboxInput(
  715. paste0("n_threshold_apply_st", s),
  716. "Apply minimum sample size filter",
  717. value = FALSE
  718. ),
  719. conditionalPanel(
  720. condition = paste0("input.n_threshold_apply_st", s, " == true"),
  721. numericInput(paste0("n_threshold_st", s),
  722. HTML("Minimum sample size (n \u2265)"),
  723. value = 10, min = 0, step = 1)
  724. ),
  725. div(class = "small-note",
  726. "Leave unchecked when no sample-size threshold applies. ",
  727. "When checked, the criterion \u201cn \u2265 [value]\u201d is ",
  728. "added to the eligibility list automatically.")
  729. ),
  730. textAreaInput(
  731. paste0("sci_custom_st", s),
  732. "Additional eligibility criteria (one per line)",
  733. rows = 3,
  734. placeholder = paste("e.g.",
  735. "Human observational studies",
  736. "Exclude animal models",
  737. "Exclude in vitro studies", sep = "\n")
  738. ),
  739. radioButtons(
  740. paste0("sci_mode_st", s),
  741. "Exclusion count entry mode:",
  742. choices = c("Total excluded (single number)" = "total",
  743. "Excluded per criterion" = "per"),
  744. selected = "total"
  745. ),
  746. uiOutput(paste0("sci_removed_ui_st", s)),
  747. uiOutput(paste0("sci_val_st", s))
  748. )
  749. )
  750. ) # end conditionalPanel
  751. }) # end lapply over MAX_ST
  752. )
  753. ), # end .sidebar-pane
  754. # ======================================================================
  755. # DRAGGABLE SPLITTER (drag to resize sidebar; double-click to reset)
  756. # ======================================================================
  757. div(class = "split-handle",
  758. title = "Drag to resize sidebar (double-click to reset)"),
  759. # ======================================================================
  760. # MAIN PANE
  761. # ======================================================================
  762. div(class = "main-pane",
  763. tabsetPanel(
  764. id = "main_tabs",
  765. # ------------------------------------------------------------------
  766. # Tab 1: Flowchart (sub-tab per subtopic)
  767. # ------------------------------------------------------------------
  768. tabPanel("Flowchart",
  769. br(),
  770. uiOutput("flow_panel")
  771. ),
  772. # ------------------------------------------------------------------
  773. # Tab 2: Counts summary
  774. # ------------------------------------------------------------------
  775. tabPanel("Counts summary",
  776. br(),
  777. uiOutput("counts_st_selector_ui"),
  778. br(),
  779. verbatimTextOutput("counts_summary")
  780. ),
  781. # ------------------------------------------------------------------
  782. # Tab 3: Methods text
  783. # ------------------------------------------------------------------
  784. tabPanel("Methods text",
  785. br(),
  786. uiOutput("methods_st_selector_ui"),
  787. br(),
  788. div(class = "methods-label", "Methods paragraph (copy into manuscript)"),
  789. verbatimTextOutput("methods_text"),
  790. br(),
  791. div(class = "methods-label", "Figure caption (copy into manuscript)"),
  792. verbatimTextOutput("figure_caption")
  793. ),
  794. # ------------------------------------------------------------------
  795. # Tab 4: About & Citation
  796. # ------------------------------------------------------------------
  797. tabPanel("About & Citation",
  798. br(),
  799. div(class = "about-h", "About this application"),
  800. div(class = "about-p",
  801. "This application generates PRISMA 2020-compliant search and ",
  802. "selection flow diagrams for systematic reviews. It supports ",
  803. "multiple independent subtopics, each with its own databases, ",
  804. "per-database technical filters, merged deduplication, and ",
  805. "configurable scientific eligibility criteria."
  806. ),
  807. div(class = "about-p",
  808. tags$b("Diagram layout (v3):"), " The flowchart uses Graphviz ",
  809. "subgraph clusters to render three coloured section bands — ",
  810. tags$b("IDENTIFICATION"), "(light blue), ",
  811. tags$b("SCREENING"), "(light amber), and ",
  812. tags$b("INCLUDED"), "(light green) — consistent with the visual ",
  813. "style of published systematic reviews. Exclusion boxes appear ",
  814. "to the right of each stage, connected with right-angle arrows ",
  815. "(splines=ortho), matching standard PRISMA 2020 flow diagrams."
  816. ),
  817. div(class = "about-h", "How to cite this tool"),
  818. div(class = "cite-block",
  819. HTML(paste0(
  820. "[Author(s)]. (", APP_DATE, "). <i>PRISMA 2020 Flow Diagram — ",
  821. "Multi-Subtopic Shiny Application</i> (Version ", APP_VERSION, "). ",
  822. "Retrieved from [repository/DOI URL]."
  823. ))
  824. ),
  825. div(class = "about-h", "PRISMA 2020 reference"),
  826. div(class = "cite-block",
  827. HTML(paste0(
  828. "Page MJ, McKenzie JE, Bossuyt PM, Boutron I, Hoffmann TC, ",
  829. "Mulrow CD, et al. (2021). The PRISMA 2020 statement: an updated ",
  830. "guideline for reporting systematic reviews. <i>BMJ</i>, 372, n71. ",
  831. "<a href='https://doi.org/10.1136/bmj.n71' target='_blank'>",
  832. "https://doi.org/10.1136/bmj.n71</a>"
  833. ))
  834. ),
  835. div(class = "about-h", "PRISMA 2020 compliance checklist"),
  836. div(class = "about-p",
  837. "When reporting your systematic review, complete the PRISMA 2020 checklist:",
  838. tags$ul(
  839. tags$li("Report the total number of records identified per database."),
  840. tags$li("Report all screening stages (technical filter, deduplication, eligibility)."),
  841. tags$li("State the number excluded at each stage with reasons."),
  842. tags$li("Report the final number of studies included in the synthesis."),
  843. tags$li(HTML("See: <a href='https://www.prisma-statement.org/prisma-2020-checklist'
  844. target='_blank'>prisma-statement.org</a>"))
  845. )
  846. ),
  847. div(class = "about-h", "Exporting diagrams"),
  848. div(class = "about-p",
  849. tags$b("SVG (vector) — best for journal submission:"),
  850. " Click ", tags$b("⬇ SVG (vector)"), " in the Flowchart tab. ",
  851. "SVG is lossless and infinitely scalable — ideal for print and ",
  852. "figure submissions. Open in Inkscape or Adobe Illustrator to ",
  853. "tweak any element before submitting."
  854. ),
  855. div(class = "about-p",
  856. tags$b("PNG @ 300 dpi — for Word / PowerPoint:"),
  857. " Click ", tags$b("⬇ PNG 300 dpi"), ". The image is rendered at ",
  858. "3 000 px wide (≈ 10 in @ 300 dpi), which meets most journal ",
  859. "figure-resolution requirements."
  860. ),
  861. div(class = "about-p",
  862. tags$b("DOT source — for manual Graphviz rendering:"),
  863. " Click ", tags$b("⬇ DOT source"), " and render with Graphviz locally:",
  864. tags$pre(style = "font-size:12px;margin-top:4px;",
  865. "dot -Tsvg prisma.dot -o prisma.svg\ndot -Tpng prisma.dot -Gdpi=300 -o prisma.png\ndot -Tpdf prisma.dot -o prisma.pdf")
  866. ),
  867. div(class = "about-p",
  868. tags$b("Quick screenshot (no extra package needed):"),
  869. " right-click the diagram → \"Save image as…\" for a screen-resolution PNG."
  870. ),
  871. div(class = "about-h", "Session information"),
  872. verbatimTextOutput("session_info")
  873. )
  874. )
  875. )
  876. )
  877. )
  878. # =============================================================================
  879. # SECTION 4 – SERVER
  880. # =============================================================================
  881. server <- function(input, output, session) {
  882. # ============================================================
  883. # 4.0 SUBTOPIC NAME INPUTS & SELECTOR
  884. # ============================================================
  885. output$st_name_inputs <- renderUI({
  886. k <- safe_int(input$k_st)
  887. tagList(lapply(seq_len(k), function(s) {
  888. textInput(paste0("st_name_", s),
  889. paste0("Subtopic ", s, " name"),
  890. value = paste0("Subtopic ", s),
  891. placeholder = "e.g. Diagnosis, Intervention, Prognosis")
  892. }))
  893. })
  894. output$st_selector_ui <- renderUI({
  895. k <- max(1L, safe_int(input$k_st))
  896. # Build current display labels for each subtopic. Reading the name
  897. # inputs DOES make this UI re-render when the user types a new name,
  898. # which is intentional (so the displayed labels stay in sync).
  899. choices_labels <- vapply(seq_len(k), function(s) {
  900. nm <- input[[paste0("st_name_", s)]] %||% paste0("Subtopic ", s)
  901. if (!nzchar(trimws(nm))) paste0("Subtopic ", s) else trimws(nm)
  902. }, character(1))
  903. # CRITICAL: preserve the current selection across re-renders.
  904. # Without isolate() here, every keystroke in a subtopic-name field
  905. # would re-create this input and snap it back to "1", which is why
  906. # users couldn't stay on Subtopic 2+.
  907. prior <- isolate(input$active_st)
  908. if (is.null(prior) || !as.character(prior) %in% as.character(seq_len(k))) {
  909. prior <- "1"
  910. }
  911. # Always render a properly Shiny-bound input (radioButtons), regardless
  912. # of k. This ensures input$active_st is always reactive and the
  913. # conditionalPanel for each subtopic evaluates correctly.
  914. div(class = "active-st-picker",
  915. radioButtons("active_st",
  916. label = NULL,
  917. choices = setNames(as.character(seq_len(k)), choices_labels),
  918. selected = prior,
  919. inline = FALSE)
  920. )
  921. })
  922. # Helper: get display name for subtopic s
  923. get_st_name <- function(s) {
  924. nm <- input[[paste0("st_name_", s)]] %||% paste0("Subtopic ", s)
  925. if (!nzchar(trimws(nm))) paste0("Subtopic ", s) else trimws(nm)
  926. }
  927. # ============================================================
  928. # 4.1 PER-SUBTOPIC HELPER FUNCTIONS (not reactives—plain fns)
  929. # These read current input values for subtopic s on demand.
  930. # ============================================================
  931. db_data_st <- function(s) {
  932. k <- safe_int(input[[paste0("k_db_st", s)]])
  933. defaults <- c("PubMed","Scopus","Web of Science","Embase","Cochrane",
  934. "PsycINFO","CINAHL","MEDLINE","Google Scholar","ProQuest",
  935. "LILACS","CNKI","WanFang","VIP","bioRxiv","medRxiv",
  936. "IEEE Xplore","ACM DL","SpringerLink","GutFinder")
  937. nm <- vapply(seq_len(k), function(i) {
  938. val <- input[[paste0("db_name_st", s, "_", i)]] %||%
  939. (if (i <= length(defaults)) defaults[i] else paste0("Database ", i))
  940. if (!nzchar(val)) paste0("Database ", i) else val
  941. }, character(1))
  942. nn <- vapply(seq_len(k), function(i)
  943. safe_int(input[[paste0("db_n_st", s, "_", i)]]), integer(1))
  944. data.frame(id = seq_len(k), name = nm, n = as.integer(nn), stringsAsFactors = FALSE)
  945. }
  946. tech_criteria_st <- function(s) {
  947. c(input[[paste0("tech_opts_st", s)]] %||% character(0),
  948. trim_lines(input[[paste0("tech_custom_st", s)]]))
  949. }
  950. sci_criteria_st <- function(s) {
  951. # (Issue 2) The n-threshold criterion is now optional. It is included
  952. # in the eligibility-criteria list only when the user has checked the
  953. # corresponding gate; otherwise the list contains only the user's
  954. # custom criteria.
  955. apply_n <- isTRUE(input[[paste0("n_threshold_apply_st", s)]])
  956. custom <- trim_lines(input[[paste0("sci_custom_st", s)]])
  957. if (apply_n) {
  958. n_val <- safe_int(input[[paste0("n_threshold_st", s)]])
  959. c(paste0("n \u2265 ", n_val), custom)
  960. } else {
  961. custom
  962. }
  963. }
  964. tech_removed_info_by_db_st <- function(s) {
  965. db <- db_data_st(s)
  966. k <- nrow(db)
  967. crit <- tech_criteria_st(s)
  968. lapply(seq_len(k), function(i) {
  969. mode <- input[[paste0("tech_mode_st", s, "_", i)]] %||% "per"
  970. if (mode == "total") {
  971. total <- safe_int(input[[paste0("tech_total_st", s, "_", i)]])
  972. list(mode = "total", crit = crit, per = rep(0L, length(crit)), total = total)
  973. } else {
  974. per <- vapply(seq_along(crit), function(j)
  975. safe_int(input[[paste0("tech_per_st", s, "_", i, "_", j)]]),
  976. integer(1))
  977. list(mode = "per", crit = crit, per = as.integer(per),
  978. total = as.integer(sum(per)))
  979. }
  980. })
  981. }
  982. sci_removed_info_st <- function(s) {
  983. mode <- input[[paste0("sci_mode_st", s)]] %||% "total"
  984. crit <- sci_criteria_st(s)
  985. if (mode == "total") {
  986. total <- safe_int(input[[paste0("sci_removed_total_st", s)]])
  987. list(mode = "total", crit = crit, per = rep(0L, length(crit)), total = total)
  988. } else {
  989. per <- vapply(seq_along(crit), function(j)
  990. safe_int(input[[paste0("sci_removed_st", s, "_", j)]]),
  991. integer(1))
  992. list(mode = "per", crit = crit, per = as.integer(per),
  993. total = as.integer(sum(per)))
  994. }
  995. }
  996. counts_st <- function(s) {
  997. db <- db_data_st(s)
  998. tech_list <- tech_removed_info_by_db_st(s)
  999. total_found_all <- sum(db$n)
  1000. tech_removed_by_db <- vapply(seq_along(tech_list),
  1001. function(i) safe_int(tech_list[[i]]$total), integer(1))
  1002. after_tech_by_db <- pmax(0L, db$n - tech_removed_by_db)
  1003. merged_after_tech <- sum(after_tech_by_db)
  1004. dup_removed <- safe_int(input[[paste0("dup_removed_st", s)]])
  1005. after_dup <- max(0L, merged_after_tech - dup_removed)
  1006. sci <- sci_removed_info_st(s)
  1007. sci_removed <- safe_int(sci$total)
  1008. final_included <- max(0L, after_dup - sci_removed)
  1009. list(
  1010. total_found_all = total_found_all,
  1011. merged_after_tech = merged_after_tech,
  1012. dup_removed = dup_removed,
  1013. after_dup = after_dup,
  1014. sci_removed = sci_removed,
  1015. final_included = final_included,
  1016. tech_removed_by_db = tech_removed_by_db,
  1017. after_tech_by_db = after_tech_by_db
  1018. )
  1019. }
  1020. # ============================================================
  1021. # 4.2 DOT BUILDER – publication-grade PRISMA 2020 layout
  1022. #
  1023. # Layout mirrors the standard published PRISMA 2020 flowchart:
  1024. # • Graphviz clusters supply coloured section bands:
  1025. # cluster_id (light blue) = IDENTIFICATION
  1026. # cluster_sc (light amber) = SCREENING
  1027. # cluster_inc (light green) = INCLUDED
  1028. # • Right-side exclusion boxes share the same rank as the
  1029. # main-flow node they branch from, producing the standard
  1030. # horizontal "→ excluded" arrows.
  1031. # • splines=ortho gives clean right-angle connectors.
  1032. # ============================================================
  1033. build_dot_st <- function(s) {
  1034. db <- db_data_st(s)
  1035. tech_list <- tech_removed_info_by_db_st(s)
  1036. cts <- counts_st(s)
  1037. sci <- sci_removed_info_st(s)
  1038. k <- nrow(db)
  1039. st_nm <- get_st_name(s)
  1040. fmt <- function(n) format(n, big.mark = ",") # number formatter
  1041. # Node-name vectors (prefix avoids DOT keyword clashes)
  1042. db_nodes <- paste0("xdb", seq_len(k))
  1043. tech_nodes <- paste0("xtech", seq_len(k))
  1044. # ── DB boxes ─────────────────────────────────────────────
  1045. db_decl <- paste(
  1046. vapply(seq_len(k), function(i) {
  1047. paste0(' ', db_nodes[i],
  1048. ' [label=', make_db_id_html(db$name[i], db$n[i],
  1049. cellpadding = 10, td_width = 160),
  1050. ', fillcolor="#FFFFFF", color="#2C5282", penwidth=1.6];')
  1051. }, character(1)), collapse = "\n")
  1052. # ── Tech-filter boxes ─────────────────────────────────────
  1053. tech_decl <- paste(
  1054. vapply(seq_len(k), function(i) {
  1055. tinfo <- tech_list[[i]]
  1056. lab <- make_filter_label_html(
  1057. title = "Technical filter",
  1058. criteria = tinfo$crit,
  1059. mode = tinfo$mode,
  1060. per_removed = tinfo$per,
  1061. total_removed = cts$tech_removed_by_db[i],
  1062. remaining = cts$after_tech_by_db[i],
  1063. included = db$n[i],
  1064. show_included = TRUE,
  1065. cellpadding = 8
  1066. )
  1067. paste0(' ', tech_nodes[i],
  1068. ' [label=', lab,
  1069. ', fillcolor="#D6E8F7", color="#2B6CB0", penwidth=1.4];')
  1070. }, character(1)), collapse = "\n")
  1071. # ── Merge box (bottom of IDENTIFICATION cluster) ──────────
  1072. xmerge_lab <- make_simple_box_html(
  1073. title = "Records after technical filters",
  1074. lines = c(
  1075. paste0("Combined: n\u2009=\u2009", fmt(cts$merged_after_tech)),
  1076. paste0("(Technical filter exclusions: ",
  1077. fmt(sum(cts$tech_removed_by_db)), ")")
  1078. ),
  1079. bold_title = TRUE, title_wrap = TRUE,
  1080. cellpadding = 12, td_width = 400
  1081. )
  1082. # ── EXCLUSION BOX 1 – right of merge ─────────────────────
  1083. # "Records removed before screening" (tech removals + duplicates)
  1084. xexcl_id_lab <- make_excl_box_html(
  1085. title = "Records removed before screening",
  1086. lines = c(
  1087. paste0("\u2022 Technical filter exclusions: n\u2009=\u2009",
  1088. fmt(sum(cts$tech_removed_by_db))),
  1089. paste0("\u2022 Duplicate records removed: n\u2009=\u2009",
  1090. fmt(cts$dup_removed))
  1091. ),
  1092. cellpadding = 10, td_width = 310
  1093. )
  1094. # ── Screened box (inside SCREENING cluster) ───────────────
  1095. xscreened_lab <- make_simple_box_html(
  1096. title = "Records screened",
  1097. lines = paste0("n\u2009=\u2009", fmt(cts$after_dup)),
  1098. bold_title = TRUE, cellpadding = 14, td_width = 400
  1099. )
  1100. # ── EXCLUSION BOX 2 – right of screened ──────────────────
  1101. # "Records excluded" with eligibility criteria breakdown
  1102. excl_lines <- {
  1103. hdr <- paste0("\u2022 Records excluded: n\u2009=\u2009", fmt(cts$sci_removed))
  1104. if (sci$mode == "per" && length(sci$crit) > 0) {
  1105. crit_rows <- paste0(" \u2013 ", sci$crit,
  1106. " (n\u2009=\u2009",
  1107. format(pmax(0L, sci$per), big.mark = ","), ")")
  1108. c(hdr, crit_rows[nzchar(sci$crit)])
  1109. } else if (length(sci$crit) > 0 && any(nzchar(sci$crit))) {
  1110. crit_str <- paste(sci$crit[nzchar(sci$crit)], collapse = "; ")
  1111. c(hdr, paste0(" Criteria: ", crit_str))
  1112. } else {
  1113. hdr
  1114. }
  1115. }
  1116. xexcl_sc_lab <- make_excl_box_html(
  1117. title = "Records excluded",
  1118. lines = excl_lines,
  1119. cellpadding = 10, td_width = 310
  1120. )
  1121. # ── Included box (inside INCLUDED cluster) ────────────────
  1122. xincluded_lab <- make_simple_box_html(
  1123. title = "Studies included in review",
  1124. lines = paste0("n\u2009=\u2009", fmt(cts$final_included)),
  1125. bold_title = TRUE, cellpadding = 16, td_width = 400
  1126. )
  1127. # ── Rank / edge strings ───────────────────────────────────
  1128. db_rank <- paste0(" { rank=same; ", paste(db_nodes, collapse = " "), " }")
  1129. tech_rank <- paste0(" { rank=same; ", paste(tech_nodes, collapse = " "), " }")
  1130. edges_db_tech <- paste(
  1131. vapply(seq_len(k), function(i)
  1132. paste0(" ", db_nodes[i], " -> ", tech_nodes[i], ";"), character(1)),
  1133. collapse = "\n")
  1134. edges_tech_merge <- paste(
  1135. paste0(" ", tech_nodes, " -> xmerge;"), collapse = "\n")
  1136. # ── Assemble DOT string ────────────────────────────────────
  1137. paste0(
  1138. 'digraph prisma {\n\n',
  1139. # ── Graph-level settings ───────────────────────────────────────────────
  1140. ' graph [\n',
  1141. ' rankdir = TB,\n',
  1142. ' splines = ortho,\n',
  1143. ' bgcolor = "#FFFFFF",\n',
  1144. ' nodesep = 1.1,\n',
  1145. ' ranksep = 0.85,\n',
  1146. ' pad = 0.55,\n',
  1147. ' compound = true,\n',
  1148. ' label = "', gv_esc(st_nm), '\\nPRISMA 2020 Flow Diagram",\n',
  1149. ' labelloc = t,\n',
  1150. ' fontname = "Helvetica-Bold",\n',
  1151. ' fontsize = 14,\n',
  1152. ' fontcolor= "#1A365D"\n',
  1153. ' ];\n\n',
  1154. # ── Default node / edge styles ─────────────────────────────────────────
  1155. ' node [\n',
  1156. ' fontname = "Helvetica",\n',
  1157. ' fontsize = 11,\n',
  1158. ' margin = "0.20,0.12",\n',
  1159. ' shape = box,\n',
  1160. ' style = "filled",\n',
  1161. ' fillcolor = "#FFFFFF",\n',
  1162. ' color = "#2C5282",\n',
  1163. ' penwidth = 1.6\n',
  1164. ' ];\n',
  1165. ' edge [\n',
  1166. ' color = "#2C5282",\n',
  1167. ' penwidth = 1.6,\n',
  1168. ' arrowsize= 0.88\n',
  1169. ' ];\n\n',
  1170. # ══════════════════════════════════════════════════════════════════════
  1171. # IDENTIFICATION cluster (light-blue band)
  1172. # Contains: DB boxes, tech-filter boxes, merge box
  1173. # ══════════════════════════════════════════════════════════════════════
  1174. ' subgraph cluster_id {\n',
  1175. ' label = <<B><FONT POINT-SIZE="13" COLOR="#1A365D">\u00A0IDENTIFICATION\u00A0</FONT></B>>;\n',
  1176. ' labeljust = l;\n',
  1177. ' labelloc = t;\n',
  1178. ' style = filled;\n',
  1179. ' fillcolor = "#EBF3FB";\n',
  1180. ' color = "#2B6CB0";\n',
  1181. ' penwidth = 2.2;\n\n',
  1182. db_decl, '\n\n',
  1183. tech_decl, '\n\n',
  1184. ' xmerge [label=', xmerge_lab,
  1185. ', fillcolor="#BFD7F0", color="#1A5E96", penwidth=1.8];\n',
  1186. db_rank, '\n',
  1187. tech_rank, '\n',
  1188. ' }\n\n',
  1189. # Exclusion box 1 (outside cluster, forced to same rank as xmerge)
  1190. ' xexcl_id [\n',
  1191. ' label = ', xexcl_id_lab, ',\n',
  1192. ' fillcolor= "#FEF2F2",\n',
  1193. ' color = "#C53030",\n',
  1194. ' penwidth = 1.5\n',
  1195. ' ];\n',
  1196. ' { rank=same; xmerge; xexcl_id }\n\n',
  1197. # ══════════════════════════════════════════════════════════════════════
  1198. # SCREENING cluster (light-amber band)
  1199. # Contains: screened box only
  1200. # ══════════════════════════════════════════════════════════════════════
  1201. ' subgraph cluster_sc {\n',
  1202. ' label = <<B><FONT POINT-SIZE="13" COLOR="#7B341E">\u00A0SCREENING\u00A0</FONT></B>>;\n',
  1203. ' labeljust = l;\n',
  1204. ' labelloc = t;\n',
  1205. ' style = filled;\n',
  1206. ' fillcolor = "#FFFAF0";\n',
  1207. ' color = "#C05621";\n',
  1208. ' penwidth = 2.2;\n\n',
  1209. ' xscreened [label=', xscreened_lab,
  1210. ', fillcolor="#FFFFFF", color="#2C5282"];\n',
  1211. ' }\n\n',
  1212. # Exclusion box 2 (outside cluster, same rank as xscreened)
  1213. ' xexcl_sc [\n',
  1214. ' label = ', xexcl_sc_lab, ',\n',
  1215. ' fillcolor= "#FEF2F2",\n',
  1216. ' color = "#C53030",\n',
  1217. ' penwidth = 1.5\n',
  1218. ' ];\n',
  1219. ' { rank=same; xscreened; xexcl_sc }\n\n',
  1220. # ══════════════════════════════════════════════════════════════════════
  1221. # INCLUDED cluster (light-green band)
  1222. # ══════════════════════════════════════════════════════════════════════
  1223. ' subgraph cluster_inc {\n',
  1224. ' label = <<B><FONT POINT-SIZE="13" COLOR="#1C4532">\u00A0INCLUDED\u00A0</FONT></B>>;\n',
  1225. ' labeljust = l;\n',
  1226. ' labelloc = t;\n',
  1227. ' style = filled;\n',
  1228. ' fillcolor = "#E8F5E9";\n',
  1229. ' color = "#276749";\n',
  1230. ' penwidth = 2.2;\n\n',
  1231. ' xincluded [label=', xincluded_lab,
  1232. ', fillcolor="#C6F6D5", color="#276749", penwidth=1.8];\n',
  1233. ' }\n\n',
  1234. # ══════════════════════════════════════════════════════════════════════
  1235. # EDGES
  1236. # ══════════════════════════════════════════════════════════════════════
  1237. ' // DB -> Tech filter (one per database)\n',
  1238. edges_db_tech, '\n\n',
  1239. ' // Tech filter -> Merge\n',
  1240. edges_tech_merge, '\n\n',
  1241. ' // Merge -> Exclusion box (tech removals + duplicates)\n',
  1242. ' xmerge -> xexcl_id;\n\n',
  1243. ' // Merge -> Screened (crosses into SCREENING cluster)\n',
  1244. ' xmerge -> xscreened;\n\n',
  1245. ' // Screened -> Exclusion box (eligibility criteria)\n',
  1246. ' xscreened -> xexcl_sc;\n\n',
  1247. ' // Screened -> Included (crosses into INCLUDED cluster)\n',
  1248. ' xscreened -> xincluded;\n',
  1249. '}'
  1250. )
  1251. }
  1252. # ============================================================
  1253. # 4.3 REGISTER ALL PER-SUBTOPIC DYNAMIC OUTPUTS
  1254. # Use local({}) to capture s correctly in each iteration
  1255. # ============================================================
  1256. for (s_outer in seq_len(MAX_ST)) {
  1257. local({
  1258. s <- s_outer
  1259. # -- DB inputs UI --
  1260. output[[paste0("db_inputs_st", s)]] <- renderUI({
  1261. k <- safe_int(input[[paste0("k_db_st", s)]])
  1262. defaults <- c("PubMed","Scopus","Web of Science","Embase","Cochrane",
  1263. "PsycINFO","CINAHL","MEDLINE","Google Scholar","ProQuest",
  1264. "LILACS","CNKI","WanFang","VIP","bioRxiv","medRxiv",
  1265. "IEEE Xplore","ACM DL","SpringerLink","GutFinder")
  1266. default_n <- c(700,800,800,800,800,300,300,500,1000,200,
  1267. 150,120,120,120,90,90,180,180,180,800)
  1268. tagList(lapply(seq_len(k), function(i) {
  1269. fluidRow(
  1270. column(6, textInput(paste0("db_name_st", s, "_", i),
  1271. paste0("DB ", i, " name"),
  1272. value = if (i <= length(defaults)) defaults[i]
  1273. else paste0("DB", i))),
  1274. column(6, numericInput(paste0("db_n_st", s, "_", i),
  1275. "Records (n)",
  1276. value = if (i <= length(default_n)) default_n[i] else 0,
  1277. min = 0, step = 1))
  1278. )
  1279. }))
  1280. })
  1281. # -- DB validation --
  1282. output[[paste0("db_val_st", s)]] <- renderUI({
  1283. db <- db_data_st(s)
  1284. if (any(db$n == 0))
  1285. div(class = "warn-box",
  1286. "\u26A0\uFE0F One or more databases has 0 records. Verify your search counts.")
  1287. })
  1288. # -- Technical filter UI (shared criteria + per-database mode/counts) --
  1289. output[[paste0("tech_ui_st", s)]] <- renderUI({
  1290. db <- db_data_st(s); k <- nrow(db)
  1291. shared_controls <- div(class = "db-box",
  1292. div(class = "db-title", "Shared technical criteria (all databases)"),
  1293. checkboxGroupInput(
  1294. paste0("tech_opts_st", s), "Pre-defined filters:",
  1295. choices = c("Human only","English only","Exclude reviews",
  1296. "Full-text available","Adult only"),
  1297. selected = c("Human only","English only")
  1298. ),
  1299. textAreaInput(
  1300. paste0("tech_custom_st", s),
  1301. "Custom criteria (one per line):",
  1302. rows = 3,
  1303. placeholder = paste0("e.g.\nPublication year \u2265 2015\n",
  1304. "Exclude conference abstracts")
  1305. ),
  1306. div(class = "small-note", "All databases default to per-criterion mode.")
  1307. )
  1308. per_db_blocks <- tagList(lapply(seq_len(k), function(i) {
  1309. div(class = "db-box",
  1310. div(class = "db-title", paste0("Database: ", db$name[i])),
  1311. radioButtons(
  1312. paste0("tech_mode_st", s, "_", i),
  1313. "Exclusion count entry:",
  1314. choices = c("Total excluded (one number)" = "total",
  1315. "Excluded per criterion" = "per"),
  1316. selected = "per"
  1317. ),
  1318. uiOutput(paste0("tech_removed_ui_st", s, "_", i))
  1319. )
  1320. }))
  1321. tagList(shared_controls, per_db_blocks)
  1322. })
  1323. # -- Tech validation --
  1324. output[[paste0("tech_val_st", s)]] <- renderUI({
  1325. db <- db_data_st(s)
  1326. cts <- counts_st(s)
  1327. over <- which(cts$tech_removed_by_db > db$n)
  1328. if (length(over) > 0)
  1329. div(class = "error-box",
  1330. paste0("\u274C Technical filter removes more records than available in: ",
  1331. paste(db$name[over], collapse = ", "), ". Please check your counts."))
  1332. })
  1333. # -- Dup validation --
  1334. output[[paste0("dup_val_st", s)]] <- renderUI({
  1335. cts <- counts_st(s)
  1336. if (cts$dup_removed > cts$merged_after_tech)
  1337. div(class = "error-box",
  1338. "\u274C Duplicates removed exceed records available after technical filters.")
  1339. })
  1340. # -- Sci removed UI --
  1341. output[[paste0("sci_removed_ui_st", s)]] <- renderUI({
  1342. mode <- input[[paste0("sci_mode_st", s)]] %||% "total"
  1343. crit <- sci_criteria_st(s)
  1344. if (mode == "total") {
  1345. numericInput(paste0("sci_removed_total_st", s),
  1346. "How many excluded (total) by eligibility filter?",
  1347. value = 0, min = 0, step = 1)
  1348. } else {
  1349. tagList(
  1350. div(class = "small-note", "Enter excluded count per criterion:"),
  1351. lapply(seq_along(crit), function(j) {
  1352. numericInput(paste0("sci_removed_st", s, "_", j),
  1353. label = crit[j], value = 0, min = 0, step = 1)
  1354. })
  1355. )
  1356. }
  1357. })
  1358. # -- Sci validation --
  1359. output[[paste0("sci_val_st", s)]] <- renderUI({
  1360. cts <- counts_st(s)
  1361. if (cts$sci_removed > cts$after_dup)
  1362. div(class = "error-box",
  1363. "\u274C Eligibility filter excludes more records than remain after deduplication.")
  1364. })
  1365. # -- Dynamic tech_removed_ui per database (up to 20 databases per subtopic) --
  1366. observe({
  1367. db <- db_data_st(s)
  1368. k_db <- nrow(db)
  1369. for (i_outer in seq_len(20)) {
  1370. local({
  1371. i <- i_outer
  1372. output[[paste0("tech_removed_ui_st", s, "_", i)]] <- renderUI({
  1373. if (i > safe_int(input[[paste0("k_db_st", s)]])) return(NULL)
  1374. mode <- input[[paste0("tech_mode_st", s, "_", i)]] %||% "per"
  1375. crit <- tech_criteria_st(s)
  1376. if (mode == "total") {
  1377. numericInput(paste0("tech_total_st", s, "_", i),
  1378. "How many excluded (total) in this database?",
  1379. value = 0, min = 0, step = 1)
  1380. } else {
  1381. if (length(crit) == 0) {
  1382. div(class = "small-note", "No criteria selected yet.")
  1383. } else {
  1384. tagList(
  1385. div(class = "small-note", "Enter excluded count per criterion:"),
  1386. lapply(seq_along(crit), function(j) {
  1387. numericInput(paste0("tech_per_st", s, "_", i, "_", j),
  1388. label = crit[j], value = 0, min = 0, step = 1)
  1389. })
  1390. )
  1391. }
  1392. }
  1393. })
  1394. })
  1395. }
  1396. })
  1397. # -- Flow diagram --
  1398. output[[paste0("flow_st", s)]] <- renderGrViz({
  1399. grViz(build_dot_st(s))
  1400. })
  1401. # -- Download: DOT source --
  1402. output[[paste0("dl_dot_st", s)]] <- downloadHandler(
  1403. filename = function() {
  1404. nm <- gsub("[^A-Za-z0-9_]", "_",
  1405. input[[paste0("st_name_", s)]] %||% paste0("st", s))
  1406. paste0("prisma_", nm, "_", format(Sys.Date(), "%Y%m%d"), ".dot")
  1407. },
  1408. content = function(file) writeLines(build_dot_st(s), file)
  1409. )
  1410. # -- Download: Counts CSV --
  1411. output[[paste0("dl_csv_st", s)]] <- downloadHandler(
  1412. filename = function() {
  1413. nm <- gsub("[^A-Za-z0-9_]", "_",
  1414. input[[paste0("st_name_", s)]] %||% paste0("st", s))
  1415. paste0("prisma_counts_", nm, "_", format(Sys.Date(), "%Y%m%d"), ".csv")
  1416. },
  1417. content = function(file) {
  1418. db <- db_data_st(s)
  1419. cts <- counts_st(s)
  1420. db_rows <- data.frame(
  1421. Stage = paste0("Identification – ", db$name),
  1422. Records_In = db$n,
  1423. Records_Removed = cts$tech_removed_by_db,
  1424. Records_Out = cts$after_tech_by_db,
  1425. Notes = "Technical filter",
  1426. stringsAsFactors = FALSE
  1427. )
  1428. summary_rows <- data.frame(
  1429. Stage = c("Merged after technical filters",
  1430. "Deduplication",
  1431. "Eligibility filter",
  1432. "Final included"),
  1433. Records_In = c(cts$merged_after_tech, cts$merged_after_tech,
  1434. cts$after_dup, cts$after_dup),
  1435. Records_Removed = c(0L, cts$dup_removed, cts$sci_removed, 0L),
  1436. Records_Out = c(cts$merged_after_tech, cts$after_dup,
  1437. cts$final_included, cts$final_included),
  1438. Notes = c("All databases merged", "Duplicate removal",
  1439. "Scientific / eligibility criteria",
  1440. "Included in synthesis"),
  1441. stringsAsFactors = FALSE
  1442. )
  1443. write.csv(rbind(db_rows, summary_rows), file, row.names = FALSE)
  1444. }
  1445. )
  1446. # -- Download: SVG vector graphic --
  1447. output[[paste0("dl_svg_st", s)]] <- downloadHandler(
  1448. filename = function() {
  1449. nm <- gsub("[^A-Za-z0-9_]", "_",
  1450. input[[paste0("st_name_", s)]] %||% paste0("st", s))
  1451. paste0("prisma_", nm, "_", format(Sys.Date(), "%Y%m%d"), ".svg")
  1452. },
  1453. content = function(file) {
  1454. req(HAS_SVG_EXPORT)
  1455. svg_str <- DiagrammeRsvg::export_svg(grViz(build_dot_st(s)))
  1456. writeLines(svg_str, file)
  1457. }
  1458. )
  1459. # -- Download: PNG @ 300 dpi (≈ 3 000 px wide) --
  1460. output[[paste0("dl_png_st", s)]] <- downloadHandler(
  1461. filename = function() {
  1462. nm <- gsub("[^A-Za-z0-9_]", "_",
  1463. input[[paste0("st_name_", s)]] %||% paste0("st", s))
  1464. paste0("prisma_", nm, "_300dpi_", format(Sys.Date(), "%Y%m%d"), ".png")
  1465. },
  1466. content = function(file) {
  1467. req(HAS_SVG_EXPORT)
  1468. svg_str <- DiagrammeRsvg::export_svg(grViz(build_dot_st(s)))
  1469. tmp_svg <- tempfile(fileext = ".svg")
  1470. on.exit(unlink(tmp_svg), add = TRUE)
  1471. writeLines(svg_str, tmp_svg)
  1472. # width = 3000 px → ~10 in @ 300 dpi; height scales proportionally
  1473. rsvg::rsvg_png(tmp_svg, file = file, width = 3000)
  1474. }
  1475. )
  1476. # -- Download: PDF (vector, publication-ready) --
  1477. output[[paste0("dl_pdf_st", s)]] <- downloadHandler(
  1478. filename = function() {
  1479. nm <- gsub("[^A-Za-z0-9_]", "_",
  1480. input[[paste0("st_name_", s)]] %||% paste0("st", s))
  1481. paste0("prisma_", nm, "_", format(Sys.Date(), "%Y%m%d"), ".pdf")
  1482. },
  1483. content = function(file) {
  1484. req(HAS_SVG_EXPORT)
  1485. svg_str <- DiagrammeRsvg::export_svg(grViz(build_dot_st(s)))
  1486. tmp_svg <- tempfile(fileext = ".svg")
  1487. on.exit(unlink(tmp_svg), add = TRUE)
  1488. writeLines(svg_str, tmp_svg)
  1489. # rsvg_pdf produces a true vector PDF (no rasterization), so the
  1490. # diagram remains crisp at any zoom level — ideal for journals.
  1491. rsvg::rsvg_pdf(tmp_svg, file = file)
  1492. }
  1493. )
  1494. }) # end local
  1495. } # end for s_outer
  1496. # ============================================================
  1497. # 4.4 MAIN OUTPUT: Flowchart panel (sub-tabs per subtopic)
  1498. # ============================================================
  1499. output$flow_panel <- renderUI({
  1500. k <- safe_int(input$k_st)
  1501. prisma_footer <- div(class = "prisma-footer",
  1502. HTML(paste0(
  1503. "<b>PRISMA 2020</b> &mdash; Page MJ, McKenzie JE, Bossuyt PM, et al. ",
  1504. "The PRISMA 2020 statement: an updated guideline for reporting systematic reviews. ",
  1505. "<i>BMJ</i>. 2021;372:n71. ",
  1506. "<a href='https://doi.org/10.1136/bmj.n71' target='_blank'>",
  1507. "https://doi.org/10.1136/bmj.n71</a>"
  1508. ))
  1509. )
  1510. if (k == 1) {
  1511. tagList(
  1512. div(class = "dl-row",
  1513. downloadButton("dl_dot_st1", "⬇ DOT source", class = "btn-dl"),
  1514. downloadButton("dl_csv_st1", "⬇ Counts CSV", class = "btn-dl gold"),
  1515. if (HAS_SVG_EXPORT)
  1516. downloadButton("dl_svg_st1", "⬇ SVG (vector)", class = "btn-dl green"),
  1517. if (HAS_SVG_EXPORT)
  1518. downloadButton("dl_png_st1", "⬇ PNG 300 dpi", class = "btn-dl teal"),
  1519. if (HAS_SVG_EXPORT)
  1520. downloadButton("dl_pdf_st1", "⬇ PDF (vector)", class = "btn-dl maroon")
  1521. ),
  1522. grVizOutput("flow_st1", height = "960px"),
  1523. br(), prisma_footer
  1524. )
  1525. } else {
  1526. st_tabs <- lapply(seq_len(k), function(s) {
  1527. nm <- input[[paste0("st_name_", s)]] %||% paste0("Subtopic ", s)
  1528. if (!nzchar(trimws(nm))) nm <- paste0("Subtopic ", s)
  1529. tabPanel(
  1530. nm,
  1531. br(),
  1532. div(class = "dl-row",
  1533. downloadButton(paste0("dl_dot_st", s),
  1534. "⬇ DOT source", class = "btn-dl"),
  1535. downloadButton(paste0("dl_csv_st", s),
  1536. "⬇ Counts CSV", class = "btn-dl gold"),
  1537. if (HAS_SVG_EXPORT)
  1538. downloadButton(paste0("dl_svg_st", s),
  1539. "⬇ SVG (vector)", class = "btn-dl green"),
  1540. if (HAS_SVG_EXPORT)
  1541. downloadButton(paste0("dl_png_st", s),
  1542. "⬇ PNG 300 dpi", class = "btn-dl teal"),
  1543. if (HAS_SVG_EXPORT)
  1544. downloadButton(paste0("dl_pdf_st", s),
  1545. "⬇ PDF (vector)", class = "btn-dl maroon")
  1546. ),
  1547. grVizOutput(paste0("flow_st", s), height = "960px"),
  1548. br(), prisma_footer
  1549. )
  1550. })
  1551. do.call(tabsetPanel, c(list(id = "flow_st_inner"), st_tabs))
  1552. }
  1553. })
  1554. # ============================================================
  1555. # 4.5 SUBTOPIC SELECTORS for Counts / Methods tabs
  1556. # ============================================================
  1557. output$counts_st_selector_ui <- renderUI({
  1558. k <- safe_int(input$k_st)
  1559. if (k > 1) {
  1560. choices_labels <- vapply(seq_len(k), function(s) {
  1561. nm <- input[[paste0("st_name_", s)]] %||% paste0("Subtopic ", s)
  1562. if (!nzchar(trimws(nm))) paste0("Subtopic ", s) else trimws(nm)
  1563. }, character(1))
  1564. selectInput("counts_st_sel", "Show counts for subtopic:",
  1565. choices = setNames(as.character(seq_len(k)), choices_labels),
  1566. selected = "1")
  1567. }
  1568. })
  1569. output$methods_st_selector_ui <- renderUI({
  1570. k <- safe_int(input$k_st)
  1571. if (k > 1) {
  1572. choices_labels <- vapply(seq_len(k), function(s) {
  1573. nm <- input[[paste0("st_name_", s)]] %||% paste0("Subtopic ", s)
  1574. if (!nzchar(trimws(nm))) paste0("Subtopic ", s) else trimws(nm)
  1575. }, character(1))
  1576. selectInput("methods_st_sel", "Generate methods text for subtopic:",
  1577. choices = setNames(as.character(seq_len(k)), choices_labels),
  1578. selected = "1")
  1579. }
  1580. })
  1581. # Active subtopic for counts/methods outputs
  1582. active_counts_s <- reactive({ safe_int(input$counts_st_sel %||% "1") })
  1583. active_methods_s <- reactive({ safe_int(input$methods_st_sel %||% "1") })
  1584. # ============================================================
  1585. # 4.6 OUTPUTS – Counts summary
  1586. # ============================================================
  1587. output$counts_summary <- renderText({
  1588. s <- active_counts_s(); if (s < 1) s <- 1L
  1589. db <- db_data_st(s)
  1590. cts <- counts_st(s)
  1591. st_nm <- get_st_name(s)
  1592. db_lines <- paste0(
  1593. vapply(seq_len(nrow(db)), function(i) {
  1594. sprintf(" %-30s identified: %d | excluded by tech filter: %d | remaining: %d",
  1595. db$name[i], db$n[i],
  1596. cts$tech_removed_by_db[i], cts$after_tech_by_db[i])
  1597. }, character(1)),
  1598. collapse = "\n"
  1599. )
  1600. paste0(
  1601. "── PRISMA 2020 RECORD COUNTS [", st_nm, "] ────────────────────────\n\n",
  1602. "IDENTIFICATION\n",
  1603. db_lines, "\n",
  1604. sprintf(" %-30s %d\n", "Total records identified:", cts$total_found_all),
  1605. "\nSCREENING\n",
  1606. sprintf(" %-30s %d\n", "After technical filters (merged):", cts$merged_after_tech),
  1607. sprintf(" %-30s %d (excluded: %d)\n",
  1608. "After deduplication:", cts$after_dup, cts$dup_removed),
  1609. "\nELIGIBILITY & INCLUSION\n",
  1610. sprintf(" %-30s %d (excluded: %d)\n",
  1611. "After eligibility filter:", cts$final_included, cts$sci_removed),
  1612. "\n────────────────────────────────────────────────────────────────────"
  1613. )
  1614. })
  1615. # ============================================================
  1616. # 4.7 OUTPUTS – Auto-generated methods text & figure caption
  1617. # ============================================================
  1618. output$methods_text <- renderText({
  1619. s <- active_methods_s(); if (s < 1) s <- 1L
  1620. db <- db_data_st(s)
  1621. cts <- counts_st(s)
  1622. sci <- sci_removed_info_st(s)
  1623. tech_crit <- tech_criteria_st(s)
  1624. st_nm <- get_st_name(s)
  1625. authors <- trimws(input$meta_authors %||% "")
  1626. title_sr <- trimws(input$meta_title %||% "this systematic review")
  1627. year_sr <- trimws(input$meta_year %||% format(Sys.Date(), "%Y"))
  1628. k <- nrow(db)
  1629. db_list <- paste0(db$name, " (n\u2009=\u2009", db$n, ")")
  1630. if (k == 1) {
  1631. db_sent <- paste0("One electronic database was searched: ", db_list[1], ".")
  1632. } else {
  1633. db_sent <- paste0(k, " electronic databases were searched: ",
  1634. paste(db_list[-k], collapse = ", "),
  1635. ", and ", db_list[k], ", yielding ",
  1636. format(cts$total_found_all, big.mark = ","),
  1637. " records in total.")
  1638. }
  1639. if (length(tech_crit) > 0) {
  1640. tech_sent <- paste0(
  1641. "Technical filters were applied per database (",
  1642. paste(tech_crit, collapse = "; "),
  1643. "), retaining ", format(cts$merged_after_tech, big.mark = ","),
  1644. " records after merging across all databases."
  1645. )
  1646. } else {
  1647. tech_sent <- paste0(
  1648. "Records from all databases were merged, yielding ",
  1649. format(cts$merged_after_tech, big.mark = ","), " records."
  1650. )
  1651. }
  1652. dup_sent <- paste0(
  1653. "Following removal of ", format(cts$dup_removed, big.mark = ","),
  1654. " duplicate record", ifelse(cts$dup_removed == 1, "", "s"), ", ",
  1655. format(cts$after_dup, big.mark = ","), " unique records were screened."
  1656. )
  1657. sci_crit_str <- if (length(sci$crit) > 0) {
  1658. paste0("eligibility criteria included: ", paste(sci$crit, collapse = "; "), ". ")
  1659. } else ""
  1660. sci_sent <- paste0(
  1661. "Eligibility screening (", sci_crit_str,
  1662. "n\u2009=\u2009", format(cts$sci_removed, big.mark = ","), " excluded) ",
  1663. "resulted in ", format(cts$final_included, big.mark = ","),
  1664. " stud", ifelse(cts$final_included == 1, "y", "ies"),
  1665. " included in the final synthesis."
  1666. )
  1667. paste0(
  1668. "Search strategy and study selection [Subtopic: ", st_nm, "]\n",
  1669. "\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500",
  1670. "\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500",
  1671. "\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n",
  1672. db_sent, " ", tech_sent, " ", dup_sent, " ", sci_sent,
  1673. "\n\nThe search and selection process was documented using a PRISMA 2020 ",
  1674. "flow diagram (Page et al., 2021). The complete search strategy and all ",
  1675. "eligibility criteria are reported in accordance with PRISMA 2020 guidelines."
  1676. )
  1677. })
  1678. output$figure_caption <- renderText({
  1679. s <- active_methods_s(); if (s < 1) s <- 1L
  1680. db <- db_data_st(s)
  1681. cts <- counts_st(s)
  1682. sci <- sci_removed_info_st(s)
  1683. k <- nrow(db)
  1684. st_nm <- get_st_name(s)
  1685. db_names_str <- if (k == 1) db$name[1] else {
  1686. paste0(paste(db$name[-k], collapse = ", "), " and ", db$name[k])
  1687. }
  1688. paste0(
  1689. "Figure. PRISMA 2020 flow diagram — ", st_nm, ". ",
  1690. "A total of ", format(cts$total_found_all, big.mark = ","),
  1691. " records were identified across ", k, " database",
  1692. ifelse(k == 1, "", "s"), " (", db_names_str, "). ",
  1693. "After per-database technical filtering, ",
  1694. format(cts$merged_after_tech, big.mark = ","),
  1695. " records were retained and merged. Removal of ",
  1696. format(cts$dup_removed, big.mark = ","), " duplicate",
  1697. ifelse(cts$dup_removed == 1, "", "s"), " yielded ",
  1698. format(cts$after_dup, big.mark = ","), " unique records for eligibility ",
  1699. "screening. Application of eligibility criteria excluded a further ",
  1700. format(cts$sci_removed, big.mark = ","), " record",
  1701. ifelse(cts$sci_removed == 1, "", "s"), ", leaving ",
  1702. format(cts$final_included, big.mark = ","), " stud",
  1703. ifelse(cts$final_included == 1, "y", "ies"),
  1704. " included in the review. ",
  1705. "Diagram produced using the PRISMA 2020 Flow Diagram Shiny application ",
  1706. "(v", APP_VERSION, "); compliant with Page et al. (2021), BMJ 372:n71."
  1707. )
  1708. })
  1709. # ============================================================
  1710. # 4.8 OUTPUT – Session info
  1711. # ============================================================
  1712. output$session_info <- renderText({
  1713. si <- sessionInfo()
  1714. paste0(
  1715. "R version: ", R.version$version.string, "\n",
  1716. "shiny: ", as.character(packageVersion("shiny")), "\n",
  1717. "DiagrammeR: ", as.character(packageVersion("DiagrammeR")), "\n",
  1718. "App version: ", APP_VERSION, " (multi-subtopic)\n",
  1719. "Platform: ", si$platform
  1720. )
  1721. })
  1722. } # end server
  1723. # =============================================================================
  1724. # LAUNCH
  1725. # =============================================================================
  1726. shinyApp(ui, server)

app.R at commit 9bfb19b, no license · at the source

Overview

Authors: Huiye Han1, Owen Luo2, Kevin Li3, Shyazana Rahaman3, Eunice Unbyul Ok3, Liangliang Zhang4, Menglu Liang3, Huang Lin3
  1. Department of Mathematics, University of Maryland, College Park, MD, United States
  2. Winston Churchill High School, Potomac, MD, United States
  3. Department of Epidemiology and Biostatistics, University of Maryland, College Park, MD, United States
  4. Department of Population and Quantitative Health Sciences, School of Medicine, Case Western Reserve University, Cleveland, OH, United States
Journal: Frontiers in cellular and infection microbiology, volume 16, article 1900578
Dates: received 4 June 2026; accepted 18 August 2026; published online 11 September 2026
Type: Systematic review · Language: English
License: CC BY
Identifiers: DOI · PMCID PMC13612191
Status: code verified
Categories: genetics / omics (modality), Parkinson's (population), cellular / molecular (subfield)
Keywords: gut–brain axis, metabolomics, microbiome, multi-omics, mycobiome, Parkinson’s disease, proteomics, virome
Citations: not cited yet (Europe PMC); 60 references in the paper

Abstract

Introduction: Parkinson's disease (PD) is increasingly recognized as a multisystem disorder in which gastrointestinal dysfunction and gut microbial alterations may contribute to disease pathophysiology. Although most microbiome research in PD has focused on bacteria, growing evidence suggests that the gut ecosystem should be considered more broadly to include fungi, viruses, metabolites, and proteins.

Methods: We searched PubMed and SciFinder for human studies published up to October 28, 2025, using domain-specific search strategies for the bacteriome, metabolome, proteome, virome, and mycobiome, and synthesized the eligible evidence using a structured multi-omics evidence-mapping framework.

Results: We summarize the most consistent bacterial findings, including enrichment of mucin-degrading taxa and depletion of short-chain fatty acid-producing commensals, and discuss how these changes relate to impaired fermentation, barrier dysfunction, and immune activation. We further examine emerging evidence for virome and mycobiome alterations, highlighting the possibility that PD-related dysbiosis reflects cross-kingdom ecological disruption rather than bacteria-only imbalance. Metabolomic studies provide functional support for this model by demonstrating altered short-chain fatty acid biology and broader host -microbe co-metabolic remodeling. Protein-focused studies, including host proteomic signatures and bacterial functional amyloids, extend the field toward mechanisms linking gut dysfunction to inflammation, proteostatic stress, and α-synuclein pathology.

Discussion: Overall, the evidence supports a multi-layer view of the PD gut -brain axis in which microbial ecology, metabolic output, barrier integrity, immune signaling, and protein-centered mechanisms are interconnected. The field remains limited by cross-sectional designs, methodological heterogeneity, and uneven evidence depth across omics layers. Longitudinal, standardized, and integrated multi-omics studies will be essential to determine which microbiome-associated alterations are mechanistically important, clinically informative, and potentially modifiable in PD.

Reproduced under the paper's license (CC BY), from the paper cited above.

Repositories

Its files are read in the Code ↔ Paper reader above, with 4 matches between paragraphs and lines of code.

hanhuiyeye/PD

License: none: the authors keep all their rights
State: the link answers, verified on 28 September 2026
Evidence: files inventoried
Commit: 7c5fbe64fb1aacf28e448f7b59f930e2a18289d9, 24 May 2026
Languages: R (1)
Size: 7 files, 1 script
Software Heritage: not archived
Found in: “Data availability statement”
Holds: README, 1 notebook
Not found: license file, CITATION.cff, environment file, tests, continuous integration, documentation
Tools: ggplot2 (1 file), tidyverse (1 file)
Availability: 1 check, the latest on 28 September 2026: the link answers
  • 28 September 2026: the link answers
2 files

euniceokk/prisma

License: none: the authors keep all their rights
State: the link answers, verified on 28 September 2026
Evidence: files inventoried
Commit: 9bfb19b436f50e76a6da133c594bbb2c9ca6dddf, 5 May 2026
Languages: R (1)
Size: 2 files, 1 script
Software Heritage: not archived
Found in: “Data availability statement”
Holds: README
Not found: license file, CITATION.cff, environment file, tests, continuous integration, documentation
Availability: 1 check, the latest on 28 September 2026: the link answers
  • 28 September 2026: the link answers
2 files

The paper's code and data availability statement is in the Data section.

Tracing map

Proposed by the machine: these links were found in the paper and verified at the source, without human review. The map will receive a Zenodo DOI once one of the paper's authors has validated it with their ORCID.

What the map holds:

  • 2 repositories of the authors' code, each at its verified commit, with its license and how the link was found in the paper;
  • 2 scripts, each with its path and the digest of its content;
  • 4 matches between paragraphs of the paper and lines of the code (method lexical-v1);
  • neither the text of the paper nor the code itself.

Its JSON (tracing-map.json) is deposited on Zenodo with its DOI once the map is validated.

Data

No dataset and no data link were found in the paper.

Data availability statement

The original contributions presented in the study are included in the article/Supplementary Material; the full list of included studies is provided in Supplementary Table 2. Code and publication-ready files for the figures are available in the public GitHub repository at https://github.com/hanhuiyeye/PD. An R Shiny application that generates PRISMA 2020 flow diagrams, developed alongside this review, is deployed at https://eok7.shinyapps.io/PRISMA/ (source code: https://github.com/euniceokk/prisma). Further inquiries can be directed to the corresponding author.

Reproduced under the paper's license (CC BY), from the paper cited above.

Versions

The history of this record: each version stored by the harvester or made by a correction of its authors or of the maintainers of its code, and what changed in its facts. The texts of the paper (its abstract, its availability statements) are not part of it; versions that changed only those are not listed.

Version 1, 28 September 2026: the first record

Recorded: type, language, journal, volume, pages, dates, 8 authors, 8 keywords, 60 references.

Cite

This paper

Han, H., Luo, O., Li, K., Rahaman, S., Ok, E. U., Zhang, L., Liang, M., & Lin, H. (2026). Beyond bacteria: a multi-omics view of the gut–brain axis in Parkinson’s disease. Frontiers in cellular and infection microbiology, 16, 1900578.

BibTeX

@article{han2026beyond,
author = {Han, Huiye and Luo, Owen and Li, Kevin and Rahaman, Shyazana and Ok, Eunice Unbyul and Zhang, Liangliang and Liang, Menglu and Lin, Huang},
title = {{Beyond bacteria: a multi-omics view of the gut–brain axis in Parkinson’s disease}},
journal = {Frontiers in cellular and infection microbiology},
year = {2026},
month = sep,
volume = {16},
pages = {1900578},
publisher = {Frontiers Media SA},
issn = {2235-2988},
pmcid = {PMC13612191}
}

RIS

TY - JOUR
AU - Han, Huiye
AU - Luo, Owen
AU - Li, Kevin
AU - Rahaman, Shyazana
AU - Ok, Eunice Unbyul
AU - Zhang, Liangliang
AU - Liang, Menglu
AU - Lin, Huang
TI - Beyond bacteria: a multi-omics view of the gut–brain axis in Parkinson’s disease
T2 - Frontiers in cellular and infection microbiology
J2 - Front Cell Infect Microbiol
PY - 2026
DA - 2026/09/26
VL - 16
SP - 1900578
SN - 2235-2988
PB - Frontiers Media SA
LA - en
ER -

CSL-JSON

{
"id": "pmcid:PMC13612191",
"type": "article-journal",
"title": "Beyond bacteria: a multi-omics view of the gut–brain axis in Parkinson’s disease",
"container-title": "Frontiers in cellular and infection microbiology",
"author": [
{
"family": "Han",
"given": "Huiye"
},
{
"family": "Luo",
"given": "Owen"
},
{
"family": "Li",
"given": "Kevin"
},
{
"family": "Rahaman",
"given": "Shyazana"
},
{
"family": "Ok",
"given": "Eunice Unbyul"
},
{
"family": "Zhang",
"given": "Liangliang"
},
{
"family": "Liang",
"given": "Menglu"
},
{
"family": "Lin",
"given": "Huang"
}
],
"container-title-short": "Front Cell Infect Microbiol",
"volume": "16",
"page": "1900578",
"PMCID": "PMC13612191",
"ISSN": "2235-2988",
"publisher": "Frontiers Media SA",
"language": "en",
"issued": {
"date-parts": [
[
2026,
9,
26
]
]
}
}

The tracing map gets a citation of its own once an author has validated it and it has a DOI.

Similar papers

The papers with a page that share the most with this one: the tools found in their code, their categories, datasets, cited references and authors, the rarest counting most.

[1] doi:10.3389/fcimb.2026.1806356
Global research trends in enteric nervous system and gut microbiota: a bibliometric analysis (2005-2025).
Journal: Frontiers in cellular and infection microbiology
In common: Parkinson's, cellular / molecular, 7 references
[2] doi:10.1038/s41531-026-01287-x [code]
Faecalibacterium prausnitzii, depleted in the Parkinson's disease microbiome, improves motor deficits in α-synuclein overexpressing mice.
Journal: NPJ Parkinson's disease
In common: tidyverse, Parkinson's, cellular / molecular, 5 references
[3] doi:10.3389/frmbi.2026.1834726 [code]
Shotgun metagenomic analysis reveals taxonomic and functional alterations in the gut microbiome across prodromal and symptomatic Lewy body disease.
Journal: Frontiers in microbiomes
In common: ggplot2, tidyverse, Parkinson's, genetics / omics, cellular / molecular, 2 references
[4] doi:10.1093/brain/awag039 [code]
Mapping the causal chain from genetic risk variants to lipid dysmetabolism in Parkinson's disease.
Journal: Brain : a journal of neurology
In common: ggplot2, tidyverse, Parkinson's, genetics / omics, 1 reference
[5] doi:10.1016/j.nbd.2026.107379 [code]
DYRK1A and Parkinson's disease, facts and hypotheses.
Journal: Neurobiology of disease
In common: ggplot2, tidyverse, Parkinson's, cellular / molecular, 1 reference
[6] doi:10.1038/s41598-026-50859-7 [code]
Prebiotic intervention changes host and microbe proteomes in plasma extracellular vesicles of Parkinson's disease.
Journal: Scientific reports
In common: Parkinson's, genetics / omics, 2 references
[7] doi:10.3389/fnagi.2026.1823372
Stereoselective effects of nicotine enantiomers on the gut-brain axis and neuroinflammation in a mouse model of Parkinson's disease.
Journal: Frontiers in aging neuroscience
In common: Parkinson's, cellular / molecular, 2 references
[8] doi:10.3389/fnagi.2026.1931183 [code]
A validated workflow for paired total and small RNA sequencing from low-input submandibular gland biopsy specimens in &lt;i&gt;de novo&lt;/i&gt; Parkinson's disease patients.
Journal: Frontiers in aging neuroscience
In common: ggplot2, tidyverse, Parkinson's, genetics / omics, 1 reference
[9] doi:10.1016/j.xgen.2026.101284 [code]
NERINE reveals rare variant associations in gene networks across phenotypes and implicates an SNCA-PRL-LRRK2 subnetwork in Parkinson's disease.
Journal: Cell genomics
In common: ggplot2, tidyverse, Parkinson's, cellular / molecular, 1 reference
[10] doi:10.1038/s41467-026-71264-8 [code]
Habitual coffee intake shapes the gut microbiome and modifies host physiology and cognition.
Journal: Nature communications
In common: ggplot2, tidyverse, genetics / omics, 1 reference

Contribute

The authors of this paper can claim it, correct its record and validate its tracing map, and the maintainers of its code (its owner, or a public member of its organization) correct what it says of their repository; anyone signed in can ask for its removal. Every request goes to OSCR's own machine, which answers it; your account page follows them.

Sign in with ORCID to claim this paper as one of its authors, correct its record or validate its tracing map: when the paper's metadata lists your ORCID iD, you are recognized at once. Maintainers of its code: sign in with GitHub, then claim the repository on your account page.

Request its removal

To ask OSCR to remove this record, the copies of its authors' scripts or its tracing map, use the removal request page: signed in, you say who you are, what to remove and why, then review and confirm the request. Published rules decide every request (how).

Discussion, reproductions, activity

Discussion: questions and error reports about this paper and its code, from signed-in readers and its authors. It opens with sign-in.

Reproductions: reports from readers who ran the authors' code: what they reproduced, with which environment, commit and data. It opens with sign-in.

Activity: what happens around this paper: new versions of its record, its map's validation, discussions and reproductions. It opens with sign-in.