COST Action CA24147 — AID-AgE: Artificial Intelligence Driven Dental Age Estimation Network
This interactive dashboard summarises the Working Group (WG) applications for the AID-AgE COST Action. It provides an at-a-glance view of applicant demographics, geographic distribution, WG membership patterns, and cross-group collaboration potential.
Data source: e-COST WG applications export (12 February 2026, 156 applicants).
Dashboard structure:
Required R packages: flexdashboard,
tidyverse, plotly, DT,
scales, RColorBrewer,
crosstalk.
To publish on RPubs: Click Knit in RStudio, then use Publish → RPubs from the preview window.
Dashboard prepared for the AID-AgE Management Committee.
---
title: "COST Action CA24147 — AID-AgE"
subtitle: "Working Group Applications Dashboard"
output:
flexdashboard::flex_dashboard:
orientation: rows
vertical_layout: scroll
theme:
version: 4
bootswatch: lux
css: !expr NULL
navbar:
- { title: "CA24147", href: "https://www.cost.eu/actions/CA24147/", align: right }
source_code: embed
---
```{css custom-styles}
/* ── Global ── */
body {
font-family: 'Source Sans Pro', 'Segoe UI', Tahoma, sans-serif;
background-color: #f8f9fc;
}
.navbar {
background-color: #0d2137 !important;
border-bottom: 3px solid #e8a838;
}
.navbar-brand {
font-weight: 700;
letter-spacing: 0.5px;
}
/* ── Value Boxes ── */
.value-box {
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
}
.value-box .inner {
padding: 12px 16px;
}
.value-box .value {
font-size: 32px;
font-weight: 700;
}
.value-box .caption {
font-size: 13px;
font-weight: 500;
text-transform: uppercase;
letter-spacing: 0.8px;
}
/* ── Chart containers ── */
.chart-wrapper {
border-radius: 8px;
box-shadow: 0 1px 6px rgba(0,0,0,0.06);
background: white;
overflow: hidden;
}
.chart-title {
font-weight: 600;
font-size: 14px;
color: #0d2137;
letter-spacing: 0.3px;
border-bottom: 2px solid #e8a838;
padding-bottom: 6px;
}
/* ── Section headers ── */
.section-title {
color: #0d2137;
font-weight: 700;
}
/* ── DT table styling ── */
.dataTables_wrapper {
font-size: 12px;
}
table.dataTable thead th {
background-color: #0d2137 !important;
color: white !important;
font-weight: 600;
font-size: 11.5px;
text-transform: uppercase;
letter-spacing: 0.5px;
}
table.dataTable tbody tr:hover {
background-color: #edf2f9 !important;
}
table.dataTable.stripe tbody tr.odd {
background-color: #f8f9fc;
}
/* ── Page navigation ── */
.nav-tabs-custom > .nav-tabs > li.active > a {
border-top-color: #e8a838;
}
```
```{r setup, include=FALSE}
# ── Load libraries ──
library(flexdashboard)
library(tidyverse)
library(plotly)
library(DT)
library(scales)
library(RColorBrewer)
library(crosstalk)
knitr::opts_chunk$set(echo = FALSE, warning = FALSE, message = FALSE)
# ── Colour palette ──
# Deep navy / amber / teal professional palette
pal_main <- "#0d2137"
pal_accent <- "#e8a838"
pal_teal <- "#1a8a7d"
pal_coral <- "#d95f5f"
pal_slate <- "#5a6f8a"
pal_light <- "#edf2f9"
# 7 distinct colours for WG1-WG7
pal_wg <- c(
"WG1" = "#2c6fbb", # Medical Imaging — blue
"WG2" = "#e8a838", # Dataset Creation — amber
"WG3" = "#1a8a7d", # AI Development — teal
"WG4" = "#d95f5f", # Clinical Validation — coral
"WG5" = "#7b5ea7", # Data Management — purple
"WG6" = "#e07830", # Dissemination — orange
"WG7" = "#3a9e5c" # Training — green
)
# ── Load data ──
# NOTE: Place the CSV in the same directory as this Rmd file, or adjust the path below.
df <- read.csv2("WG_applications_export_12-02-2026.csv",
fileEncoding = "UTF-8-BOM",
stringsAsFactors = FALSE,
sep = ";",
quote = '"')
# ── Clean column names ──
wg_cols <- grep("^WG", names(df), value = TRUE)
wg_labels <- c(
"WG1" = "Medical Imaging Acquisition",
"WG2" = "Dataset Creation & Annotation",
"WG3" = "AI Development & Engineering",
"WG4" = "Clinical Validation",
"WG5" = "Data Management & Privacy",
"WG6" = "Dissemination & Communication",
"WG7" = "Training & Education"
)
# Short names for each WG column
wg_short <- paste0("WG", 1:7)
names(wg_cols) <- wg_short
# Convert y/n to logical
for (col in wg_cols) {
df[[col]] <- tolower(trimws(df[[col]])) == "y"
}
# Clean country field (remove quotes)
df$country <- gsub('"', '', df$country)
# Extract ISO code from country string, e.g. "Türkiye (TR)" → "TR"
df$country_iso <- gsub(".*\\(([A-Z]{2})\\).*", "\\1", df$country)
df$country_name <- gsub("\\s*\\([A-Z]{2}\\)\\s*", "", df$country)
# Number of WGs per applicant
df$n_wgs <- rowSums(df[, wg_cols])
# Clean affiliation
df$affiliation <- gsub('^"|"$', '', df$affiliation)
```
# Overview {data-icon="fa-chart-bar"}
## Row {data-height=120}
### Total Applicants {.value-box}
```{r}
valueBox(nrow(df), icon = "fa-users", color = pal_main)
```
### Approved {.value-box}
```{r}
n_approved <- sum(df$status == "approved")
valueBox(n_approved, icon = "fa-check-circle", color = pal_teal)
```
### Countries {.value-box}
```{r}
valueBox(n_distinct(df$country_iso), icon = "fa-globe", color = pal_accent)
```
### Female Applicants {.value-box}
```{r}
pct_f <- round(100 * mean(df$gender == "Female"), 1)
valueBox(paste0(pct_f, "%"), icon = "fa-venus", color = pal_coral)
```
### Young Researchers {.value-box}
```{r}
pct_yr <- round(100 * mean(tolower(df$youngResearcher) == "y"), 1)
valueBox(paste0(pct_yr, "%"), icon = "fa-graduation-cap", color = pal_slate)
```
### ITC Countries {.value-box}
```{r}
pct_itc <- round(100 * mean(tolower(df$itc) == "y"), 1)
valueBox(paste0(pct_itc, "%"), icon = "fa-flag", color = "#7b5ea7")
```
## Row {data-height=420}
### Working Group Membership
```{r}
# Count members per WG
wg_counts <- tibble(
wg = wg_short,
label = wg_labels[wg_short],
n = sapply(wg_cols, function(col) sum(df[[col]]))
)
wg_counts <- wg_counts %>% arrange(desc(n))
p1 <- plot_ly(wg_counts,
x = ~reorder(wg, n), y = ~n,
type = "bar",
marker = list(
color = pal_wg[wg_counts$wg],
line = list(color = pal_main, width = 1)
),
text = ~paste0(wg, ": ", label, "<br><b>", n, " applicants</b>"),
hoverinfo = "text") %>%
layout(
xaxis = list(title = "", tickfont = list(size = 12, family = "Source Sans Pro")),
yaxis = list(title = "Applicants", gridcolor = "#eee"),
margin = list(b = 40),
plot_bgcolor = "white",
paper_bgcolor = "white"
) %>%
config(displayModeBar = FALSE)
p1
```
### Applicants by Country (Top 15)
```{r}
country_counts <- df %>%
count(country_name, country_iso, sort = TRUE) %>%
head(15) %>%
arrange(n)
p2 <- plot_ly(country_counts,
y = ~reorder(paste0(country_name, " (", country_iso, ")"), n),
x = ~n,
type = "bar",
orientation = "h",
marker = list(
color = colorRampPalette(c(pal_light, pal_main))(15),
line = list(color = pal_main, width = 0.5)
),
text = ~n,
textposition = "outside",
hoverinfo = "y+x") %>%
layout(
xaxis = list(title = "Applicants", gridcolor = "#eee"),
yaxis = list(title = "", tickfont = list(size = 11)),
margin = list(l = 160),
plot_bgcolor = "white",
paper_bgcolor = "white"
) %>%
config(displayModeBar = FALSE)
p2
```
## Row {data-height=400}
### WG Membership per Applicant
```{r}
wg_dist <- df %>%
count(n_wgs) %>%
mutate(pct = round(100 * n / sum(n), 1))
p3 <- plot_ly(wg_dist,
x = ~factor(n_wgs), y = ~n,
type = "bar",
marker = list(
color = c(pal_slate, pal_accent, pal_teal, pal_coral,
"#7b5ea7", "#e07830", "#3a9e5c")[1:nrow(wg_dist)],
line = list(color = pal_main, width = 0.8)
),
text = ~paste0(n, " (", pct, "%)"),
textposition = "outside",
hoverinfo = "text") %>%
layout(
xaxis = list(title = "Number of WGs joined"),
yaxis = list(title = "Applicants", gridcolor = "#eee"),
plot_bgcolor = "white",
paper_bgcolor = "white"
) %>%
config(displayModeBar = FALSE)
p3
```
### Application Status
```{r}
status_df <- df %>%
count(status) %>%
mutate(pct = round(100 * n / sum(n), 1))
p4 <- plot_ly(status_df,
labels = ~status,
values = ~n,
type = "pie",
hole = 0.55,
textinfo = "label+value+percent",
textfont = list(size = 13, family = "Source Sans Pro"),
marker = list(
colors = c("approved" = pal_teal, "submitted" = pal_accent)[status_df$status],
line = list(color = "white", width = 2)
),
hoverinfo = "label+value+percent") %>%
layout(
showlegend = FALSE,
annotations = list(
text = paste0("<b>", nrow(df), "</b><br>Total"),
showarrow = FALSE, font = list(size = 16, color = pal_main)
),
plot_bgcolor = "white",
paper_bgcolor = "white"
) %>%
config(displayModeBar = FALSE)
p4
```
### Gender × Young Researcher
```{r}
cross_df <- df %>%
mutate(YR = ifelse(tolower(youngResearcher) == "y", "Young Researcher", "Established")) %>%
count(gender, YR)
p5 <- plot_ly(cross_df,
x = ~gender, y = ~n,
color = ~YR,
colors = c("Young Researcher" = pal_accent, "Established" = pal_main),
type = "bar",
text = ~n,
textposition = "inside",
hoverinfo = "x+text+name") %>%
layout(
barmode = "stack",
xaxis = list(title = ""),
yaxis = list(title = "Applicants", gridcolor = "#eee"),
legend = list(orientation = "h", y = -0.15, x = 0.2),
plot_bgcolor = "white",
paper_bgcolor = "white"
) %>%
config(displayModeBar = FALSE)
p5
```
# WG Analysis {data-icon="fa-project-diagram"}
## Row {data-height=480}
### WG Co-occurrence Heatmap
```{r fig.height=5}
# Build co-occurrence matrix
co_mat <- matrix(0, nrow = 7, ncol = 7,
dimnames = list(wg_short, wg_short))
for (i in 1:7) {
for (j in 1:7) {
co_mat[i, j] <- sum(df[[wg_cols[i]]] & df[[wg_cols[j]]])
}
}
# Off-diagonal only for heatmap (diagonal = WG size)
co_display <- co_mat
diag(co_display) <- NA
# Custom hover text
hover_text <- matrix("", 7, 7)
for (i in 1:7) {
for (j in 1:7) {
if (i == j) {
hover_text[i,j] <- paste0(wg_short[i], " total: ", co_mat[i,j])
} else {
hover_text[i,j] <- paste0(wg_short[i], " ∩ ", wg_short[j], ": ", co_mat[i,j], " shared members")
}
}
}
p6 <- plot_ly(
z = co_display,
x = wg_short, y = wg_short,
type = "heatmap",
colorscale = list(c(0, "#edf2f9"), c(0.5, "#5a9bd5"), c(1, "#0d2137")),
text = hover_text,
hoverinfo = "text",
showscale = TRUE,
colorbar = list(title = "Shared\nMembers", len = 0.6)
) %>%
layout(
xaxis = list(title = "", tickfont = list(size = 12)),
yaxis = list(title = "", tickfont = list(size = 12), autorange = "reversed"),
margin = list(l = 50, t = 30),
plot_bgcolor = "white",
paper_bgcolor = "white"
) %>%
config(displayModeBar = FALSE)
# Add text annotations
for (i in 1:7) {
for (j in 1:7) {
val <- co_mat[i, j]
p6 <- p6 %>% add_annotations(
x = wg_short[j], y = wg_short[i],
text = val,
showarrow = FALSE,
font = list(
size = 11,
color = ifelse(i == j, pal_accent, ifelse(val > 25, "white", pal_main)),
family = "Source Sans Pro"
)
)
}
}
p6
```
### Country Representation per WG
```{r fig.height=5}
# Build long-form data: each row = one WG membership for one person
wg_country <- bind_rows(
lapply(1:7, function(i) {
df %>%
filter(!!sym(wg_cols[i])) %>%
transmute(wg = wg_short[i], country_name, country_iso)
})
)
# Top 10 countries overall
top10 <- df %>% count(country_name, sort = TRUE) %>% head(10) %>% pull(country_name)
wg_country_top <- wg_country %>%
filter(country_name %in% top10) %>%
count(wg, country_name) %>%
mutate(country_name = factor(country_name, levels = rev(top10)))
p7 <- plot_ly(wg_country_top,
x = ~n, y = ~country_name,
color = ~wg,
colors = pal_wg,
type = "bar",
orientation = "h",
hoverinfo = "x+name+y") %>%
layout(
barmode = "stack",
xaxis = list(title = "Applicants"),
yaxis = list(title = ""),
legend = list(orientation = "h", y = -0.12, x = 0, font = list(size = 10)),
margin = list(l = 140),
plot_bgcolor = "white",
paper_bgcolor = "white"
) %>%
config(displayModeBar = FALSE)
p7
```
## Row {data-height=420}
### Gender Balance by WG
```{r}
gender_wg <- bind_rows(
lapply(1:7, function(i) {
df %>%
filter(!!sym(wg_cols[i])) %>%
transmute(wg = wg_short[i], gender)
})
) %>%
count(wg, gender) %>%
group_by(wg) %>%
mutate(pct = round(100 * n / sum(n), 1)) %>%
ungroup()
p8 <- plot_ly(gender_wg,
x = ~wg, y = ~pct,
color = ~gender,
colors = c("Female" = pal_coral, "Male" = pal_main),
type = "bar",
text = ~paste0(n, " (", pct, "%)"),
hoverinfo = "text+name") %>%
layout(
barmode = "stack",
xaxis = list(title = ""),
yaxis = list(title = "Percentage", range = c(0, 105), gridcolor = "#eee"),
legend = list(orientation = "h", y = -0.12, x = 0.3),
shapes = list(
list(type = "line", x0 = -0.5, x1 = 6.5, y0 = 50, y1 = 50,
line = list(color = pal_accent, width = 1.5, dash = "dot"))
),
plot_bgcolor = "white",
paper_bgcolor = "white"
) %>%
config(displayModeBar = FALSE)
p8
```
### Young Researcher Ratio by WG
```{r}
yr_wg <- bind_rows(
lapply(1:7, function(i) {
df %>%
filter(!!sym(wg_cols[i])) %>%
transmute(
wg = wg_short[i],
yr_status = ifelse(tolower(youngResearcher) == "y", "Young Researcher", "Established")
)
})
) %>%
count(wg, yr_status) %>%
group_by(wg) %>%
mutate(pct = round(100 * n / sum(n), 1)) %>%
ungroup()
p9 <- plot_ly(yr_wg,
x = ~wg, y = ~pct,
color = ~yr_status,
colors = c("Young Researcher" = pal_teal, "Established" = pal_slate),
type = "bar",
text = ~paste0(n, " (", pct, "%)"),
hoverinfo = "text+name") %>%
layout(
barmode = "stack",
xaxis = list(title = ""),
yaxis = list(title = "Percentage", range = c(0, 105), gridcolor = "#eee"),
legend = list(orientation = "h", y = -0.12, x = 0.2),
plot_bgcolor = "white",
paper_bgcolor = "white"
) %>%
config(displayModeBar = FALSE)
p9
```
### ITC Representation by WG
```{r}
itc_wg <- bind_rows(
lapply(1:7, function(i) {
df %>%
filter(!!sym(wg_cols[i])) %>%
transmute(
wg = wg_short[i],
itc_status = ifelse(tolower(itc) == "y", "ITC", "Non-ITC")
)
})
) %>%
count(wg, itc_status) %>%
group_by(wg) %>%
mutate(pct = round(100 * n / sum(n), 1)) %>%
ungroup()
p10 <- plot_ly(itc_wg,
x = ~wg, y = ~pct,
color = ~itc_status,
colors = c("ITC" = "#7b5ea7", "Non-ITC" = "#c4b5d9"),
type = "bar",
text = ~paste0(n, " (", pct, "%)"),
hoverinfo = "text+name") %>%
layout(
barmode = "stack",
xaxis = list(title = ""),
yaxis = list(title = "Percentage", range = c(0, 105), gridcolor = "#eee"),
legend = list(orientation = "h", y = -0.12, x = 0.3),
plot_bgcolor = "white",
paper_bgcolor = "white"
) %>%
config(displayModeBar = FALSE)
p10
```
# Applicant Directory {data-icon="fa-address-book"}
## Row
### Full Applicant Database {data-height=800}
```{r}
# Build display table
tbl <- df %>%
mutate(
Name = paste(firstName, lastName),
WGs = apply(df[, wg_cols], 1, function(x) paste(wg_short[x], collapse = ", ")),
ITC = ifelse(tolower(itc) == "y", "Yes", "No"),
YR = ifelse(tolower(youngResearcher) == "y", "Yes", "No"),
Country = country_name,
Status = status
) %>%
select(Name, affiliation, Country, Status, gender, YR, ITC, WGs, n_wgs) %>%
rename(
Affiliation = affiliation,
Gender = gender,
`Young Res.` = YR,
`WG Count` = n_wgs,
`Working Groups` = WGs
) %>%
arrange(Name)
datatable(
tbl,
filter = "top",
rownames = FALSE,
extensions = c("Buttons", "Scroller"),
options = list(
dom = "Bfrtip",
buttons = c("csv", "excel", "pdf"),
scrollY = "520px",
scroller = TRUE,
pageLength = 156,
autoWidth = TRUE,
columnDefs = list(
list(width = "140px", targets = 0),
list(width = "180px", targets = 1),
list(width = "80px", targets = 2),
list(width = "55px", targets = c(3, 4, 5, 6, 8)),
list(width = "130px", targets = 7)
),
language = list(
search = "Search applicants:",
info = "Showing _START_ to _END_ of _TOTAL_ applicants"
)
)
) %>%
formatStyle("Status",
backgroundColor = styleEqual(
c("approved", "submitted"),
c("#e8f5e9", "#fff8e1")
),
fontWeight = "bold"
) %>%
formatStyle("WG Count",
background = styleColorBar(c(0, 7), pal_teal),
backgroundSize = "95% 70%",
backgroundRepeat = "no-repeat",
backgroundPosition = "center"
)
```
# WG1 {data-icon="fa-x-ray" data-navmenu="Working Groups"}
## Row
### WG1 — Medical Imaging Acquisition {data-height=600}
```{r}
wg_table <- function(wg_index) {
col <- wg_cols[wg_index]
df %>%
filter(!!sym(col)) %>%
mutate(
Name = paste(firstName, lastName),
Country = country_name,
YR = ifelse(tolower(youngResearcher) == "y", "Yes", "No"),
ITC = ifelse(tolower(itc) == "y", "Yes", "No"),
Status = status,
Expertise = substr(scientificExpertise, 1, 120)
) %>%
select(Name, affiliation, Country, Status, gender, YR, ITC, Expertise) %>%
rename(Affiliation = affiliation, Gender = gender, `Young Res.` = YR) %>%
arrange(Name)
}
datatable(
wg_table(1),
filter = "top",
rownames = FALSE,
options = list(
dom = "frtip",
pageLength = 60,
scrollY = "450px",
autoWidth = TRUE
)
) %>%
formatStyle("Status",
backgroundColor = styleEqual(c("approved", "submitted"), c("#e8f5e9", "#fff8e1")),
fontWeight = "bold"
)
```
# WG2 {data-icon="fa-database" data-navmenu="Working Groups"}
## Row
### WG2 — Dataset Creation & Annotation {data-height=600}
```{r}
datatable(
wg_table(2), filter = "top", rownames = FALSE,
options = list(dom = "frtip", pageLength = 70, scrollY = "450px", autoWidth = TRUE)
) %>%
formatStyle("Status",
backgroundColor = styleEqual(c("approved", "submitted"), c("#e8f5e9", "#fff8e1")),
fontWeight = "bold"
)
```
# WG3 {data-icon="fa-microchip" data-navmenu="Working Groups"}
## Row
### WG3 — AI Development & Engineering {data-height=600}
```{r}
datatable(
wg_table(3), filter = "top", rownames = FALSE,
options = list(dom = "frtip", pageLength = 75, scrollY = "450px", autoWidth = TRUE)
) %>%
formatStyle("Status",
backgroundColor = styleEqual(c("approved", "submitted"), c("#e8f5e9", "#fff8e1")),
fontWeight = "bold"
)
```
# WG4 {data-icon="fa-stethoscope" data-navmenu="Working Groups"}
## Row
### WG4 — Clinical Validation {data-height=600}
```{r}
datatable(
wg_table(4), filter = "top", rownames = FALSE,
options = list(dom = "frtip", pageLength = 60, scrollY = "450px", autoWidth = TRUE)
) %>%
formatStyle("Status",
backgroundColor = styleEqual(c("approved", "submitted"), c("#e8f5e9", "#fff8e1")),
fontWeight = "bold"
)
```
# WG5 {data-icon="fa-shield-alt" data-navmenu="Working Groups"}
## Row
### WG5 — Data Management & Privacy {data-height=600}
```{r}
datatable(
wg_table(5), filter = "top", rownames = FALSE,
options = list(dom = "frtip", pageLength = 40, scrollY = "450px", autoWidth = TRUE)
) %>%
formatStyle("Status",
backgroundColor = styleEqual(c("approved", "submitted"), c("#e8f5e9", "#fff8e1")),
fontWeight = "bold"
)
```
# WG6 {data-icon="fa-bullhorn" data-navmenu="Working Groups"}
## Row
### WG6 — Dissemination & Communication {data-height=600}
```{r}
datatable(
wg_table(6), filter = "top", rownames = FALSE,
options = list(dom = "frtip", pageLength = 60, scrollY = "450px", autoWidth = TRUE)
) %>%
formatStyle("Status",
backgroundColor = styleEqual(c("approved", "submitted"), c("#e8f5e9", "#fff8e1")),
fontWeight = "bold"
)
```
# WG7 {data-icon="fa-chalkboard-teacher" data-navmenu="Working Groups"}
## Row
### WG7 — Training & Education {data-height=600}
```{r}
datatable(
wg_table(7), filter = "top", rownames = FALSE,
options = list(dom = "frtip", pageLength = 80, scrollY = "450px", autoWidth = TRUE)
) %>%
formatStyle("Status",
backgroundColor = styleEqual(c("approved", "submitted"), c("#e8f5e9", "#fff8e1")),
fontWeight = "bold"
)
```
# About {data-icon="fa-info-circle"}
## Row {data-height=300}
### About this Dashboard
**COST Action CA24147 — AID-AgE: Artificial Intelligence Driven Dental Age Estimation Network**
This interactive dashboard summarises the Working Group (WG) applications for the AID-AgE COST Action. It provides an at-a-glance view of applicant demographics, geographic distribution, WG membership patterns, and cross-group collaboration potential.
**Data source:** e-COST WG applications export (12 February 2026, `r nrow(df)` applicants).
**Dashboard structure:**
- **Overview** — Key performance indicators, WG sizes, country distribution, status and demographics.
- **WG Analysis** — Co-occurrence heatmap showing cross-WG membership, country breakdown per WG, gender balance, Young Researcher and ITC ratios across all seven groups.
- **Applicant Directory** — Searchable, filterable, exportable table of all applicants with their WG memberships and profile attributes.
- **Working Groups** (dropdown) — Dedicated filterable tables for each WG (WG1–WG7) with expertise summaries.
**Required R packages:** `flexdashboard`, `tidyverse`, `plotly`, `DT`, `scales`, `RColorBrewer`, `crosstalk`.
**To publish on RPubs:** Click *Knit* in RStudio, then use *Publish* → *RPubs* from the preview window.
*Dashboard prepared for the AID-AgE Management Committee.*