diff --git a/apps/data_library/app-text.R b/apps/data_library/app-text.R new file mode 100644 index 0000000..6e760c8 --- /dev/null +++ b/apps/data_library/app-text.R @@ -0,0 +1,25 @@ +# Demo App Text +# Contains text displayed on the landing page tabs. + +# About Page Text +welcome_text = "This demo application allows you to explore and search the reports and datasets + available in the Demo platform from one central location. +

+ Select Reports or Data Catalog from the sidebar to begin + exploring, or click the '+' to expand the boxes below to learn more." + +reports_welcome_text = "The Reports page contains links to and information about the different + interactive dashboards and other analyses hosted in the Demo platform. Scroll to browse + through the reports or use the Technical Area and Tags filters in the sidebar + to narrow your selection. +

+ Click on the report title to be directed to the report in a new tab." + +data_welcome_text = "The Data Catalog page contains information about datasets available in the Demo platform. + Scroll to browse through the datasets or use the Technical Area and Tags filters + in the sidebar to narrow your selection. +

+ Each dataset card includes a brief description, last updated date, and access + restrictions." + +help_welcome_text = "This is a demo app that demonstrates how an organization could use the Civis Platform" diff --git a/apps/data_library/app.R b/apps/data_library/app.R new file mode 100644 index 0000000..1302731 --- /dev/null +++ b/apps/data_library/app.R @@ -0,0 +1,439 @@ +# M-DIVE Landing Page — Demo Version +# Derived from apps/mdive_landing_page/app.R + +install.packages("shinycssloaders") + +library(shiny) +library(shinydashboard) +library(stringr) +library(dplyr) +library(shinyBS) +library(shinyWidgets) +library(shinycssloaders) +library(shinyjs) + +source('components.R') +source('app-text.R') +source('demo-data.R') + +# ── Derived filter choices ──────────────────────────────────────────────────── + +tag_choices <- get_unique_choices(reports_data, 'tags', data, 'tags') +tag_choices_br <- stringr::str_wrap(tag_choices, width = 35) +tag_choices_br <- stringr::str_replace_all(tag_choices_br, "\n", "
") + +tech_area_choices <- get_unique_choices(reports_data, 'technical_area', data, 'technical_area') +tech_area_choices_br <- stringr::str_wrap(tech_area_choices, width = 35) +tech_area_choices_br <- stringr::str_replace_all(tech_area_choices_br, "\n", "
") + +# ── UI ──────────────────────────────────────────────────────────────────────── + +ui <- function(request) { + dashboardPage(skin = 'black', + + dashboardHeader(title = "Welcome to the Demo Dashboard", titleWidth = 350), + + dashboardSidebar( + sidebarMenu( + id = "tabs", + div(class = 'sidebar-menu', id = "sidebar_nav", + menuItem("About", tabName = "home"), + menuItem("Demo Reports", tabName = "reports", selected = TRUE), + menuItem("Demo Data Catalog", tabName = "overview"), + # Hidden — only reachable by clicking a dataset card + conditionalPanel("input.tabs == 'something'", + menuItem("Data Details", tabName = "data_details")) + ), + div(class = 'sidebar-menu', id = 'sidebar_filters', + conditionalPanel( + "input.tabs == 'reports' | input.tabs == 'overview'", + pickerInput( + width = '200px', + inputId = "tech_area", + label = HTML("Filter by
Technical Area"), + choices = sort_other(tech_area_choices), + multiple = TRUE, + selected = NULL, + choicesOpt = list( + style = rep_len("color:black;", length(tech_area_choices)), + content = sort_other(tech_area_choices_br) + ), + options = pickerOptions( + actionsBox = TRUE, + liveSearch = TRUE, + dropupAuto = FALSE, + size = 8, + iconBase = "", + tickIcon = "fa fa-check", + `selected-text-format` = "count", + `count-selected-text` = "{0} of {1} Technical Areas Chosen" + ) + ), + pickerInput( + width = '200px', + inputId = "dataset_choice", + label = HTML("Or filter by
Tags"), + choices = sort_other(tag_choices), + multiple = TRUE, + selected = NULL, + choicesOpt = list( + style = rep_len("color:black;", length(tag_choices)), + content = sort_other(tag_choices_br) + ), + options = pickerOptions( + actionsBox = TRUE, + liveSearch = TRUE, + dropupAuto = FALSE, + size = 8, + iconBase = "", + tickIcon = "fa fa-check", + `selected-text-format` = "count", + `count-selected-text` = "{0} of {1} Tags Chosen" + ) + ) + ) + ) + ) + ), + + dashboardBody( + shinyjs::useShinyjs(), + tags$script("document.getElementsByClassName('sidebar-toggle')[0].style.visibility = 'hidden';"), + includeCSS('styles.css'), + + tabItems( + + # ── About Tab ────────────────────────────────────────────────────── + tabItem(tabName = 'home', + fluidRow(shinydashboard::box( + id = 'welcome_flip', status = "info", + title = span(icon("home", lib = "font-awesome"), ' Welcome'), + width = 12, closable = FALSE, collapsible = TRUE, + solidHeader = TRUE, collapsed = FALSE, + p(HTML(welcome_text)) + )), + br(), + fluidRow(shinydashboard::box( + id = 'report_flip', status = "info", + title = span(icon("chart-line", lib = "font-awesome"), + actionLink('reports_go', ' Demo Reports', + style = 'text-decoration:underline')), + width = 12, closable = FALSE, collapsible = TRUE, + solidHeader = TRUE, collapsed = TRUE, + p(HTML(reports_welcome_text)) + )), + br(), + fluidRow(shinydashboard::box( + id = 'data_flip', status = "info", + title = span(icon("database", lib = "font-awesome"), + actionLink('data_go', ' Demo Data Catalog', + style = 'text-decoration:underline')), + width = 12, closable = FALSE, collapsible = TRUE, + solidHeader = TRUE, collapsed = TRUE, + p(HTML(data_welcome_text)) + )), + br(), + fluidRow(shinydashboard::box( + id = 'help_flip', status = "info", + title = span(icon("question", lib = "font-awesome"), " Help"), + width = 12, closable = FALSE, collapsible = TRUE, + solidHeader = TRUE, collapsed = TRUE, + p(HTML(help_welcome_text)) + )) + ), + + # ── Demo Reports Tab ───────────────────────────────────────────── + tabItem(tabName = 'reports', + div(id = "demo_reports", + style = 'overflow-x:hidden;overflow-y:scroll;max-height:600px;width:100%;padding-right:4px', + fluidRow( + column(6, + textInput("search_box_reports", label = "Search", + placeholder = "Search...", width = '86.5%')), + column(6, + pickerInput( + inputId = 'order_results_reports', + label = "Order by", + choices = c( + 'Featured' = 'featured', + 'Report Name: A-Z' = 'name', + 'Category' = 'category' + ), + choicesOpt = list(style = rep_len("color:black;", 3)) + )) + ), + uiOutput('reportHeading'), + uiOutput('reports_page') %>% withSpinner(color = "#2474a4") + ) + ), + + # ── Data Details Tab ─────────────────────────────────────────────── + tabItem(tabName = 'data_details', + dataDetailsUI(id = 'data_details_tab', + dataset_options = unique(data$name)) + ), + + # ── Data Catalog Tab ─────────────────────────────────────────────── + tabItem(tabName = 'overview', + fluidPage( + fluidRow( + column(6, + textInput("search_box", label = "Search", + placeholder = "Search...", width = '86.5%')), + column(6, + selectInput('order_results', "Order by", + choices = c( + 'Dataset Name: A-Z' = 'name', + 'Last Updated: Oldest to Newest' = 'last_data_update' + ))) + ) + ), + fluidPage( + div(style = 'overflow-x:hidden;overflow-y:scroll;max-height:600px;width:100%;padding-right:4px', + id = "data_catalog", + uiOutput('cardHeading'), + uiOutput('cardContents') %>% withSpinner(color = "#2474a4")) + ) + ) + + ) # end tabItems + ) # end dashboardBody + ) +} + +# ── Server ──────────────────────────────────────────────────────────────────── + +server <- function(input, output, session) { + + # ── Filtered Data Reactives ─────────────────────────────────────────────── + + filtered_data <- reactive({ + tag_selections <- add_backslash_to_string(input$dataset_choice) + tech_area_selections <- add_backslash_to_string(input$tech_area) + + output <- data + + if (!is.null(input$dataset_choice)) { + output <- output %>% dplyr::filter(str_detect(tags, tag_selections)) + } + + if (!is.null(input$tech_area)) { + output <- output %>% dplyr::filter(str_detect(technical_area, tech_area_selections)) + } + + if (!is.na(input$search_box) & input$search_box != "") { + output <- output %>% + filter_all(any_vars(agrepl(input$search_box, ., ignore.case = TRUE))) + } + + if (!is.null(input$order_results)) { + output <- output %>% arrange_at(.vars = c(input$order_results, 'name')) + } + + output + }) + + filtered_reports <- reactive({ + tag_selections <- add_backslash_to_string(input$dataset_choice) + tech_area_selections <- add_backslash_to_string(input$tech_area) + + output <- reports_data + + if (!is.null(input$dataset_choice)) { + output <- output %>% dplyr::filter(str_detect(tags, tag_selections)) + } + + if (!is.null(input$tech_area)) { + output <- output %>% dplyr::filter(str_detect(technical_area, tech_area_selections)) + } + + if (!is.na(input$search_box_reports) & input$search_box_reports != "") { + output <- output %>% + filter_all(any_vars(agrepl(input$search_box_reports, ., ignore.case = TRUE))) + } + + if (!is.null(input$order_results_reports)) { + output <- output %>% arrange_at(.vars = c(input$order_results_reports, 'name')) + } + + output + }) + + # ── Reports Tab ─────────────────────────────────────────────────────────── + + output$reports_page <- renderUI({ + if (nrow(filtered_reports()) >= 1) { + + results <- lapply(1:nrow(filtered_reports()), function(rownum) { + boxButtonUI( + id = str_replace_all( + filtered_reports()[rownum, 'name'], + c(':' = '', ',' = '', '-' = '', ' ' = '_', '\\(' = '', '\\)' = '', '&' = '') + ), + box_link = filtered_reports()[rownum, 'location_link'], + development_status = filtered_reports()[rownum, 'development_status'], + description = filtered_reports()[rownum, 'description'], + tooltip_text = "Click to open this report in a new tab", + tag_list = strsplit(filtered_reports()[rownum, 'tags'], ';') %>% unlist() %>% trimws('both'), + tag_filter_list = input$dataset_choice, + tech_area_list = strsplit(filtered_reports()[rownum, 'technical_area'], ';') %>% unlist() %>% trimws('both'), + tech_area_filter_list = input$tech_area + ) + }) + + # Insert category/featured headers when sorting by category or featured + if (input$order_results_reports %in% c('category')) { + category_counts <- filtered_reports() %>% + dplyr::mutate(position = row_number()) %>% + dplyr::group_by(!!dplyr::sym(input$order_results_reports)) %>% + dplyr::mutate(group_start_position = row_number()) %>% + dplyr::filter(group_start_position == 1) %>% + dplyr::select(!!sym(input$order_results_reports), position) + + for (i in 1:nrow(category_counts)) { + insert_position <- category_counts$position[i] + i - 1 + html_headers <- strong(h3(category_counts[[input$order_results_reports]][[i]])) + results <- append(results, list(html_headers), insert_position - 1) + } + } + + } else { + results <- fluidRow(width = 12, + actionLink("reset_input", "Reset your filters to view more results.", + style = "margin-left: 15px")) + } + results + }) + + dataset_choices <- reactive(input$dataset_choice) + tech_area_input_choices <- reactive(input$tech_area) + + lapply(1:nrow(reports_data), function(rownum) { + boxButtonServer( + id = str_replace_all( + reports_data[rownum, 'name'], + c(':' = '', ',' = '', '-' = '', ' ' = '_', '\\(' = '', '\\)' = '', '&' = '') + ), + link = reports_data[rownum, 'location_link'], + box_title_text = h3(span(reports_data[rownum, 'name'], + icon("external-link-alt", lib = "font-awesome"))), + tag_list = strsplit(reports_data[rownum, 'tags'], ';') %>% unlist() %>% trimws('both'), + tag_filter_list = dataset_choices, + tech_area_list = strsplit(reports_data[rownum, 'technical_area'], ';') %>% unlist() %>% trimws('both'), + tech_area_filter_list = tech_area_input_choices, + parent_session = session + ) + }) + + # ── Data Catalog Tab ────────────────────────────────────────────────────── + + output$cardContents <- renderUI({ + if (nrow(filtered_data()) >= 1) { + lapply(1:nrow(filtered_data()), function(rownum) { + resultsCardUI( + str_replace_all( + filtered_data()[rownum, 'name'], + c(':' = '', ',' = '', '-' = '', ' ' = '_', '\\(' = '', '\\)' = '', '&' = '') + ), + filtered_data(), rownum, + tag_filter_list = input$dataset_choice, + tech_area_filter_list = input$tech_area + ) + }) + } else { + fluidRow(width = 12, + actionLink("reset_input", "Reset your filters to view more results.", + style = "margin-left: 15px")) + } + }) + + lapply(1:nrow(data), function(rownum) { + resultsCardServer( + str_replace_all( + data[rownum, 'name'], + c(':' = '', ',' = '', '-' = '', ' ' = '_', '\\(' = '', '\\)' = '', '&' = '') + ), + parent_session = session, + data = data, + row = rownum, + target_module = 'data_details_tab', + tag_filter_list = dataset_choices, + tech_area_filter_list = tech_area_input_choices + ) + }) + + dataDetailsServer('data_details_tab', data = data, reports_data = reports_data) + + # ── Headings ────────────────────────────────────────────────────────────── + + output$cardHeading <- renderUI({ + tech_picks <- paste(input$tech_area, collapse = ', ') + tag_picks <- paste(input$dataset_choice, collapse = ', ') + + if (nrow(filtered_data()) == 0) { + out <- "Sorry! No datasets match selected filters." + } else { + out <- paste0("Showing ", nrow(filtered_data()), " dataset(s)") + } + + if (tech_picks != "" & tag_picks == "" & nrow(filtered_data()) > 0) + out <- HTML(out, " tagged with ", tech_picks, " technical area(s)") + if (tech_picks == "" & tag_picks != "" & nrow(filtered_data()) > 0) + out <- HTML(out, " tagged with ", tag_picks, ".") + if (tag_picks != "" & tech_picks != "" & nrow(filtered_data()) > 0) + out <- HTML(out, " tagged with ", tag_picks, " AND ", tech_picks, " technical area(s)") + + out + }) + + output$reportHeading <- renderUI({ + tech_picks <- paste(input$tech_area, collapse = ', ') + tag_picks <- paste(input$dataset_choice, collapse = ', ') + + if (nrow(filtered_reports()) == 0) { + out <- "Sorry! No reports match selected filters." + } else { + out <- paste0("Showing ", nrow(filtered_reports()), " report(s)") + } + + if (tech_picks != "" & tag_picks == "" & nrow(filtered_reports()) > 0) + out <- HTML(out, " tagged with ", tech_picks, " technical area(s)") + if (tech_picks == "" & tag_picks != "" & nrow(filtered_reports()) > 0) + out <- HTML(out, " tagged with ", tag_picks, ".") + if (tag_picks != "" & tech_picks != "" & nrow(filtered_reports()) > 0) + out <- HTML(out, " tagged with ", tag_picks, " AND ", tech_picks, " technical area(s)") + + out + }) + + # ── Navigation observers ────────────────────────────────────────────────── + + observeEvent(input$reset_input, { + updatePickerInput(session, "dataset_choice", selected = "") + updatePickerInput(session, "tech_area", selected = "") + updateTextInput(session, "search_box", value = "") + updateTextInput(session, "search_box_reports", value = "") + }) + + observeEvent(input$reports_go, { + updateTabItems(session, "tabs", "reports") + }) + + observeEvent(input$data_go, { + updateTabItems(session, "tabs", "overview") + }) + + observeEvent(input$go_back_button, { + updateTabItems(session, "tabs", "overview") + }) + + # Tab bookmarking + observeEvent(reactiveValuesToList(input), session$doBookmark()) + onBookmarked(updateQueryString) + observe({ + setBookmarkExclude(dplyr::setdiff(names(input), "tabs")) + }) +} + +#### App #### +shinyApp(ui, server, enableBookmarking = "url") diff --git a/apps/data_library/components.R b/apps/data_library/components.R new file mode 100644 index 0000000..af551cb --- /dev/null +++ b/apps/data_library/components.R @@ -0,0 +1,277 @@ +# Demo App - UI Components +# Derived from apps/mdive_landing_page/components.R + +# Inlined from style_files/18f_custom_ui.R +actionButton18F <- function(..., theme = 'light') { + actionButton(..., class = ifelse(theme == 'light', 'btn-18f-light', 'btn-18f-dark')) +} + + +#' Adds "//" to "(" and ")" characters so str_detect works correctly for filtering +#' @param x a string +add_backslash_to_string = function(x){ + v = paste(x, collapse = '|') + v = gsub("(", "\\(", v, fixed = TRUE) + v = gsub(")", "\\)", v, fixed = TRUE) +} + + +#' Gets unique categories from a ';'-separated column across both reports and data inventories. +get_unique_choices = function(reports_data, report_col, data, data_col){ + + report_choices <- unlist(lapply(reports_data[report_col], function(x) strsplit(x, ';'))) %>% + trimws('both') %>% + unique() + + catalog_choices <- unlist(lapply(data[data_col], function(x) strsplit(x, ';'))) %>% + trimws('both') %>% + unique() + + all_choices <- unique(c(report_choices, catalog_choices)) + + return(all_choices) +} + + +#' Orders a vector so 'Other' appears at the bottom. +sort_other = function(unique_choices){ + if ('Other' %in% unique_choices){ + choices = unique_choices[!unique_choices == 'Other'] + choices = sort(choices) + choices = c(choices, 'Other') + } else { + choices = sort(unique_choices) + } + return(choices) +} + + +create_buttons = function(text_list, filter_list, ns, tag_separator = ';'){ + + button_tags <- tagList() + + if (length(text_list) == 0){ + button_tags = "None" + } else { + text_list_length <- length(text_list) + i <- 1 + for (text in text_list) { + if (i == text_list_length) { + text_label <- text + } else { + text_label <- paste0(text, tag_separator) + } + if (text != '') { + btn_class <- ifelse(text %in% filter_list, + 'btn-18f-selected', + 'btn-18f') + button_text <- ifelse( + text %in% filter_list, + 'Click to remove from filter', + 'Click to add to filter' + ) + button_tags <- tagList(button_tags, + actionLink( + inputId = ns(str_replace_all(text, "[^A-Za-z]", "")), + label = text_label, + ), + bsTooltip(ns(str_replace_all(text, "[^A-Za-z]", "")), button_text) + ) + } + i <- i + 1 + } + } + + return(button_tags) +} + + +update_input = function(input, text_list, filter_list, parent_session, input_id) { + lapply( + text_list, + function(text) { + observeEvent(input[[str_replace_all(text, "[^A-Za-z,]", "")]], { + if (text %in% filter_list()) { + new_list <- filter_list()[!filter_list() == text] + } else { + new_list <- c(filter_list(), text) + } + updatePickerInput(parent_session, input_id, selected = new_list) + }) + } + ) +} + + +resultsCardUI <- function(id, input_data, rownum, + tag_filter_list, tech_area_filter_list) { + + ns <- NS(id) + + tag_list <- input_data[rownum, 'tags'] %>% str_split(';') %>% unlist() %>% trimws('both') + tech_area_list <- input_data[rownum, 'technical_area'] %>% str_split(';') %>% unlist() %>% trimws('both') + + tag_buttons = create_buttons(tag_list, tag_filter_list, ns) + tech_area_buttons = create_buttons(tech_area_list, tech_area_filter_list, ns) + + wellPanel(style = "background-color:white; color:black", + bsTooltip(id = ns('link_title'), title = 'Click to view full description and data details', + placement = "right", trigger = 'hover', options = list(container = "body")), + div(class = 'row results-header', style = 'padding-left:12px', + actionLink(inputId = ns('link_title'), label = input_data[rownum, "name"], + style = 'margin-top:0px; text-decoration:underline') + ), + div(class = 'row results-body', + div(class = 'col-sm-8 results-body-main', + p(HTML(str_trunc(gsub("\n", "
", input_data[rownum, "description"]), + width = 450, + ellipsis = "...(Click Dataset Name to See Full Description)"))) + ), + div(class = 'col-sm-4 results-body-side border-left', + h4("Data Updated on:"), + p(input_data[rownum, "clean_last_data_update"]), + h4("Access Restrictions"), + p(input_data[rownum, "access_restrictions"])) + ), + div(class = 'results-details', + 'Technical area:', tech_area_buttons, + br(), + 'Tags:', tag_buttons + ) + ) +} + + +resultsCardServer <- function(id, parent_session, data, row, target_module, + tag_filter_list, tech_area_filter_list) { + ns <- NS(target_module) + + moduleServer(id, function(input, output, session) { + tag_list <- data[row, 'tags'] %>% str_split(';') %>% unlist() %>% trimws('both') + tech_area_list <- data[row, 'technical_area'] %>% str_split(';') %>% unlist() %>% trimws('both') + + update_input(input, tag_list, tag_filter_list, parent_session, 'dataset_choice') + update_input(input, tech_area_list, tech_area_filter_list, parent_session, 'tech_area') + + table_selection <- reactive({ data[row, "name"] }) + + observeEvent(input$link_title, { + updateTabItems(session = parent_session, "tabs", "data_details") + updateSelectInput(session = parent_session, ns('dataset'), selected = table_selection()) + }) + }) +} + + +boxButtonUI <- function(id, box_link, description, tooltip_text, development_status, + tag_list, tag_filter_list = c(), + tech_area_list, tech_area_filter_list = c()) { + ns <- NS(id) + + tag_buttons = create_buttons(tag_list, tag_filter_list, ns) + tech_area_buttons = create_buttons(tech_area_list, tech_area_filter_list, ns) + + box( + id = ns('box_info'), + status = 'primary', + title = tags$div( + style = 'display:inline-block', + uiOutput(ns("box_title")), + class = 'report-header' + ), + div(class = 'results-body-report-main', style = 'border-bottom:1px solid rgb(241, 241, 241)', + tags$p(HTML(' Status: ', development_status)), + tags$p(description) + ), + bsTooltip(id = ns('box_title'), title = tooltip_text, + placement = "right", trigger = 'hover', options = list(container = "body")), + div(class = 'results-body-report-details', + div(id = ns("icon_box"), 'Technical Area:', tech_area_buttons), + div(id = ns("icon_box"), 'Tags:', tag_buttons) + ), + width = 12 + ) +} + + +# ── Data Details Tab ────────────────────────────────────────────────────────── + +dataDetailsUI <- function(id, dataset_options) { + ns <- NS(id) + fluidPage( + fluidRow( + column(8, actionButton("go_back_button", "← Back to Data Catalog")), + column(4, selectInput(ns("dataset"), "Select dataset", + choices = sort(dataset_options))) + ), + br(), + uiOutput(ns("details_content")) + ) +} + +dataDetailsServer <- function(id, data, reports_data) { + moduleServer(id, function(input, output, session) { + + output$details_content <- renderUI({ + req(input$dataset) + r <- data[data$name == input$dataset, ] + req(nrow(r) > 0) + + assoc <- r$associated_reports[1] + has_assoc <- length(assoc) == 1 && !is.na(assoc) && nchar(trimws(assoc)) > 0 + assoc_ui <- if (has_assoc) { + report_names <- trimws(strsplit(assoc, ";")[[1]]) + tags$ul(lapply(report_names, function(rn) { + report_row <- reports_data[reports_data$name == rn, ] + if (nrow(report_row) > 0) { + tags$li(tags$a(href = report_row$location_link[1], rn, target = "_blank")) + } else { + tags$li(rn) + } + })) + } else { + p("None") + } + + box( + width = 12, status = "primary", + h3(r$name), + p(HTML(gsub("\n", "
", r$full_description))), + tags$hr(), + fluidRow( + column(6, + h4("Technical Area"), p(r$technical_area), + h4("Last Updated"), p(r$clean_last_data_update), + h4("Access Restrictions"), p(r$access_restrictions) + ), + column(6, + h4("Source"), p(r$source), + h4("Unit of Analysis"), p(r$unit_of_analysis), + h4("Associated Reports"), assoc_ui + ) + ) + ) + }) + }) +} + + +# Simplified: all reports are always accessible in the demo (no access gate) +boxButtonServer <- function(id, link, box_title_text, + tag_list = c(), tag_filter_list = NULL, + tech_area_list = c(), tech_area_filter_list = NULL, + parent_session = NULL) { + moduleServer(id, function(input, output, session) { + + update_input(input, tag_list, tag_filter_list, parent_session, 'dataset_choice') + update_input(input, tech_area_list, tech_area_filter_list, parent_session, 'tech_area') + + output$box_title <- renderUI({ + tags$a( + href = link, + HTML(paste(tags$u(box_title_text))), + target = '_blank' + ) + }) + }) +} diff --git a/apps/data_library/demo-data.R b/apps/data_library/demo-data.R new file mode 100644 index 0000000..7e37ae5 --- /dev/null +++ b/apps/data_library/demo-data.R @@ -0,0 +1,119 @@ +# Demo App - Mock Data +# All static data used by the demo app lives here. + +# ── Reports ─────────────────────────────────────────────────────────────────── + +reports_data <- data.frame( + name = c( + "Quarterly Report", + "Facility Level Dashboard", + "Cases Dashboard", + "Disease in Pregnancy Dashboard", + "Treatment Dashboard" + ), + description = c( + "Tracks confirmed cases and reporting rates across a selection of countries on a quarterly basis.", + "Monitors reporting rates and case data at the facility level for targeted follow-up.", + "Summarizes case data across supported regions.", + "Monitors disease in pregnancy indicators across supported regions.", + "Tracks treatment indicators across supported regions." + ), + report_type = c("Shiny", "Shiny", "Shiny", "Shiny", "Tableau"), + development_status = c("Production", "Production", "In Development", "Production", "Production"), + technical_area = c("Reporting", "Reporting", "Measurement", "Measurement", "Treatment"), + tags = c( + "quarterly;cases;reporting", + "facility;reporting;cases", + "cases;measurement", + "disease;pregnancy;measurement", + "treatment;cases" + ), + category = c("General", "General", "Interventions", "Interventions", "Interventions"), + featured = c(1, 2, 3, 4, 5), + location_link = rep("#", 5), + stringsAsFactors = FALSE +) +# ── Data Catalog ────────────────────────────────────────────────────────────── + +data <- data.frame( + name = c( + "QR Reporting Table", + "Facility Master List", + "Inventory Stock Data", + "Program Coverage Data", + "Population Estimates" + ), + description = c( + "Monthly case and reporting data aggregated from national health information systems across supported countries.", + "Master list of all supported health facilities including location and administrative hierarchy.", + "Logistics data including commodity stockouts, consumption, and stock on hand.", + "Campaign coverage data by cycle and administrative unit.", + "Annual population estimates by administrative unit used for rate calculations." + ), + full_description = c( + "The QR Reporting Table contains monthly surveillance data compiled from national health information systems + across all supported countries. It includes confirmed cases, suspected cases, and testing data broken down + by administrative unit (admin 1 and admin 2), facility, and month. Reporting rates and completeness + indicators are also included to help users assess data quality. This table is the primary data source + for the Quarterly Report dashboard.", + "The Facility Master List is the authoritative reference for all supported health facilities. + It includes facility names, unique identifiers, administrative hierarchy (country, admin 1, admin 2), + GPS coordinates where available, facility type, and ownership category. + It is used as a lookup table across multiple dashboards to ensure consistent facility naming + and geographic attribution.", + "The Inventory Stock Data table contains commodity tracking information from national logistics + information systems. It covers stock on hand, quantities received, quantities consumed, and stockout + days for key commodities. Data is reported at the facility level on a monthly basis. + Access is restricted due to the sensitivity of supply chain information.", + "The Program Coverage Data table contains campaign-level coverage data for seasonal intervention + programs. Each row represents one administrative unit in one campaign cycle, with fields for + target population, individuals reached, and coverage percentage. + Data is available for all supported countries with active programs.", + "The Population Estimates table provides annual population denominators by administrative unit + used across the platform for calculating rate-based indicators (e.g. cases per 1,000 population). + Estimates are derived from national census projections and are updated annually." + ), + technical_area = c("Case Management", "Health Systems", "Supply Chain", "Campaigns", "Cross-cutting"), + tags = c( + "surveillance;cases;reporting", + "facilities;geo;admin", + "stockouts;commodities;logistics", + "coverage;campaigns", + "population;denominators" + ), + access_restrictions = c( + "Open use within platform", + "Open use within platform", + "Restricted", + "Open use within platform", + "Publicly Available" + ), + clean_last_data_update = c( + "January 2026", "December 2025", "November 2025", "October 2025", "January 2026" + ), + last_data_update = as.Date(c( + "2026-01-01", "2025-12-01", "2025-11-01", "2025-10-01", "2026-01-01" + )), + source = c( + "National Health Information System", + "Country Program Teams", + "National Logistics System", + "National Program Office", + "National Census Projections" + ), + unit_of_analysis = c( + "Country / Admin 1 / Admin 2 / Month", + "Facility", + "Facility / Month", + "Admin unit / Campaign cycle", + "Admin unit / Year" + ), + associated_reports = c( + "Quarterly Report; Facility Level Dashboard", + "Facility Level Dashboard", + "", + "Cases Dashboard", + "Quarterly Report; Disease in Pregnancy Dashboard" + ), + stringsAsFactors = FALSE +) diff --git a/apps/data_library/styles.css b/apps/data_library/styles.css new file mode 100644 index 0000000..fc242a4 --- /dev/null +++ b/apps/data_library/styles.css @@ -0,0 +1,565 @@ +/* +This file specifies 18F guidelines for the MDIVE landing page +It adjusts the outline color of boxes, dashboard sidebar color, +hover color for dashboard navigation, etc. It is similar to the file +used in the QR Dashboard and contains some specifications that will only +be necessary if we start using submenus in the MDIVE landing page +*/ + +/****************************** Navbar/Top Header ******************************/ + /* Header background color for logo and text formatting */ + .main-header .logo { + background-color: #1c304a !important; + color:white !important; + height: 70px; + font-size: 30px; + font-weight: 500; + padding-top: 10px; + border:0 !important; + } + + .main-header .logo:hover { + background-color: #f4b943; + } + + /* navbar (rest of the header) */ + .main-header .navbar { + background-color: #1c304a !important; + color:white !important; + height: 70px; + } + + /* Sidebar toggle button - 18F white */ + .main-header .navbar .sidebar-toggle{ + color: white !important; + border:0 !important; + padding-top: 30px; + } + + /* Sidebar toggle button when hovered - 18Fmedium blue hover */ + .main-header .navbar .sidebar-toggle:hover{ + color: white !important; + background-color:#034c6d !important; + border:0; + } + + /* header button - 18F Dark */ + .btn-18f-header { + background: none; + color:white; + padding-top:30px; + margin-right:40px; + border: none; + touch-action: manipulation; + } + + .btn-header-lessmarg { + background: none; + color:white; + padding-top:10px; + border: none; + touch-action: manipulation; + } + .btn-header-lessmarg:hover { + background: none; + color:white; + padding-top:10px; + border: none; + touch-action: manipulation; + } + + + + .btn-18f-header:hover { + background: none; + color:white; + padding-top:30px; + margin-right:40px; + border: none; + touch-action: manipulation; + } + +/****************************** Sidebar ******************************/ + /* Sidebar background color - 18F medium blue*/ + .main-sidebar { + background-color: #046b99 !important; + padding-top: 70px; + } + /* Sidebar navigation font - 18F h2*/ + .main-sidebar .sidebar .sidebar-menu #sidebar_nav li{ + font-size: 20px; + font-weight: 600; + } + + /* Sidebar filters title font - 18F p*/ + .main-sidebar .sidebar .sidebar-menu .control-label { + font-size: 14px; + font-weight: normal; + } + + /* Sidebar filters display font - 18F p*/ + .main-sidebar .sidebar .sidebar-menu .filter-option { + font-size: 14px; + font-weight: normal; + margin-left: 10px; + } + + + /* Sidebar filters dropdown font - 18F p*/ + .main-sidebar .sidebar .sidebar-menu #sidebar_filters li{ + font-size: 14px; + } + + /* Activate selected tab in sidebar - 18F medium blue hover */ + .main-sidebar .sidebar .sidebar-menu .active a{ + background-color: #034c6d; + } + + /* Other links in sidebar when hovered - 18F medium blue hover */ + .main-sidebar .sidebar .sidebar-menu a:hover{ + background-color: #034c6d; + } + + /* Treeview - 18F medium blue*/ + .main-sidebar .sidebar .sidebar-menu .treeview-menu { + background-color: #046b99; + font-size:14px; + } + + .main-sidebar .sidebar .sidebar-menu .treeview-menu .active { + background-color: #034c6d; + font-size:14px; + color: white; + font-weight: 600; + + } + /* Treeview active - 18F medium blue*/ + .main-sidebar .sidebar .sidebar-menu .treeview li.active a { + background-color: #034c6d !important; + font-size:14px; + color: white; + font-weight: 600; + } + + /* Treeview menu sidebar - 18F medium hover blue*/ + .main-sidebar .sidebar .sidebar-menu .treeview-menu a { + background-color: #046b99 !important; + } + + /* Treeview menu hover - 18F medium blue*/ + .main-sidebar .sidebar .sidebar-menu .treeview-menu a:hover { + background-color: #034c6d !important; + } + + + /* Sidebar menu dropdown hover - 18F lightest gray */ + .main-sidebar .sidebar .sidebar-menu .dropdown{ + background-color: #f1f1f1; + color: black !important; + } + +/*Remove arrow from sidebar */ +.main-sidebar .sidebar .sidebar-menu .fa-angle-left:before { + content: ""; +} + /* Sidebar menu dropdown hover - 18F lightest gray */ + .main-sidebar .sidebar .sidebar-menu .dropdown a:hover{ + background-color: #f1f1f1; + color: black !important; + } + + /* Sidebar menu dropdown active - 18F lightest gray */ + .main-sidebar .sidebar .sidebar-menu .dropdown-menu .active a{ + background-color: #f1f1f1; + color: black !important; + } + +/****************************** Body ******************************/ + + /* Backgrond color for body - 18F white */ + .content-wrapper { + background-color: white !important; + } + .wrapper { + background-color: white !important; + } + label{ + font-weight: 400; + } + + /* Margins for tabs and default text set to p */ + .tab-content .tab-pane { + padding-top: 5px; + padding-left: 20px; + padding-right: 10px; + font-size: 14px; + } + + + /* Border for boxes - 18F lightest gray */ + .box.box-primary { + margin-top: 10px; + margin-bottom: 25px; + border:3px solid #f1f1f1; + border-left-width:3px; + } + + /* Well border (in case the app calls something a well instead of box) - 18F lightest gray */ + .tab-content .tab-pane .well { + margin-top: 10px; + margin-bottom: 10px; + border: 3px solid #f1f1f1; + padding-bottom: 5px; + } + + /*Boxes for 'home' page*/ + .box.box-solid.box-info>.box-header { + color:white; + background:#046b99 +} + .box.box-solid.box-info>.box-header .box-title { + font-size: 20px; +} + + .box.box-solid.box-info{ + background-color: #e6e6e6; + border-bottom-color:#046b99; + border-left-color:#046b99; + border-right-color:#046b99; + border-top-color:#046b99; + } + + /* Font size for header of boxes - h4 */ + .tab-content .tab-pane .box.box-primary .box-header { + font-size: 21px; + font-weight: 600; + padding-bottom: 5px; + } + + /* Font size for body of boxes - p */ + .tab-content .tab-pane .box.box-primary .box-body { + font-size: 14px; + font-weight: normal; + color: black; + padding-top: 5px; + } + + /*Warning boxes*/ + .box.box-solid.box-warning>.box-header { + color:white; + background:#f39c12; +} + + .box.box-solid.box-warning>.box-header .btn{ + background:#f39c12 !important; + } + + /* Font size for header of wells - h4 */ + .tab-content .tab-pane .well .row.results-header { + font-size: 21px; + font-weight: 600; + padding-bottom: 5px; + } + + /* Font size for body of wells - p */ + .tab-content .tab-pane .well .row.results-body { + font-size: 14px; + font-weight: normal; + color: black; + } + + .tab-content .tab-pane .well .results-details { + font-size: 14px; + font-weight: normal; + color: black; + border-top:1px solid rgb(241, 241, 241); + padding-left:12px; + } + +/* Body filters title font - 18F p*/ + .tab-content .tab-pane .control-label { + color: black; + font-size: 14px; + font-weight: normal; + } + +/* Body filters display font - 18F p*/ + .tab-content .tab-pane .selectize-input{ + color: black; + font-size: 14px; + font-weight: normal; + background-color: #f1f1f1; + } + +/* Body filters dropdown font - 18F p*/ + .tab-content .tab-pane .selectize-dropdown{ + color: black; + font-size: 14px; + font-weight: normal; + background-color: white; + } + +/* Body filters dropdown hover - 18F p*/ + .tab-content .tab-pane .selectize-dropdown .active{ + color: black; + font-size: 14px; + font-weight: normal; + background-color: #f1f1f1; + } + +/* DataTables text default size and weight - 18F p*/ + + .tab-content .tab-pane .dataTables_wrapper{ + font-size: 14px; + font-weight: normal; + } + + .card .avatar { + background: #E0E0E0 !important; + color: black !important; + border: 0px; + border-radius:0px; + } + .card { + background-color: #E0E0E0 !important; + font: white !important; + } + + /*Slider selection on Sidebar */ + .irs-grid-text { + color: white !important; + } + + .irs-grid-pol { + background-color: white !important; +} +/****************************** Font Style and Sizes ******************************/ + /* font - 18F Helvetica */ + .sansserif { + font-family: Helvetica, sans-serif; + } + + + h1 { + font-size: 34px; + font-weight: 600; + } + + h2 { + font-size: 26px; + font-weight: 600; + } + + h3 { + font-size: 18px; + font-weight: 600; + margin-top:0px; + } + + h4 { + font-size: 17px; + font-weight: 600; + } + + h5 { + font-size: 16px; + font-weight: 600; + } + + +/****************************** Objects ******************************/ + /* align text to center */ + .center { + display: block; + margin-left: auto; + margin-right: auto; + width: 80%; + } + + /* hyperlink color - 18F medium blue*/ + a { + color:#046b99; + } + + /* hyperlink hover color - 18F medium hover blue*/ + a:hover { + color:#034c6d; + } + + /* Style for scrollbar */ + ::-webkit-scrollbar { + -webkit-appearance: none; + width: 7px; + } + + /* Style for the scrolling part of the scrolbar */ + ::-webkit-scrollbar-thumb { + border-radius: 4px; + background-color: rgba(0, 0, 0, .5); + box-shadow: 0 0 1px rgba(255, 255, 255, .5); + } + + + /* medium button w/ white background (for preview buttons) */ + .btn-18f-medium { + color: #046b99; + font-size: 14px; + background: none; + padding: 0; + border: none; + } + /* medium button w/ white background (for preview buttons) - hover */ + .btn-18f-medium:hover { + color: #034c6d; + font-size: 14px; + background: none; + padding: 0; + border: none; + } + + /* dark button - 18F medium */ + .btn-18f-dark { + background-color:#046b99; + color:white; + font-size: 14px; + } + + /* dark button hover - 18F medium hover */ + .btn-18f-dark:hover { + background-color:#034c6d; + color:white; + font-size: 14px; + } + + /* sidebar dark button hover */ + .btn-18f-side { + background-color:#034c6d; + color:white; + border: none; + font-size: 14px; + } + /* sidebar dark button - hover */ + .btn-18f-side:hover { + background-color:white; + color:#034c6d; + border: none; + font-size: 14px; + } + /* sidebar dark button - selected */ + .btn-18f-side:focus { + background-color: white; + color:#034c6d; + border: none; + } + + + /* light button - 18F medium */ + .btn-18f-light { + background-color:#046b99; + color:white; + font-size: 14px; + } + + /* light button hover - 18F medium hover */ + .btn-18f-light:hover { + background-color:#034c6d; + color:white; + font-size: 14px; + } + + +/* tooltips - white background and black outline and text */ + .tooltip-inner { + background-color: white; + color: black; + font-size: 14px; + border: 1px solid black; + } + + /* light button in main tab pane - 18F medium*/ + .tab-content .tab-pane .btn{ + color: white; + font-size: 14px; + font-weight: normal; + background-color: #046b99; + } + + +/* light button hover in main tab pane - 18F medium hover*/ + .tab-content .tab-pane .btn:hover { + color: white; + font-size: 14px; + font-weight: normal; + background-color: #034c6d; + } + + .tab-content .tab-pane .btn-18f-selected { + color: white; + font-size: 14px; + font-weight: normal; + background-color: #034c6d; + } + .tab-content .tab-pane .btn-default.active{ + color: white; + font-size: 14px; + font-weight: normal; + background-color: #034c6d; + } + +/*Dropdown menu in main pane for pickerInput style */ + .tab-content .tab-pane .dropdown-toggle { + background-color: #f1f1f1; + color: black; + } + .tab-content .tab-pane .dropdown-toggle:hover { + background-color: #f1f1f1; + color: black; + } + .tab-content .tab-pane .dropdown-menu .inner { + font-size: 14px; + } + .tab-content .tab-pane .dropdown-menu .li .a:hover { + font-size: 14px; + background-color: #f1f1f1 !important; + } + + .dropdown-menu li a { + font-size: 14px; + color: black; + } + .tab-content .tab-pane .dropdown-menu .active { + font-size: 14px; + background-color: white; + color: #777; + } + .tab-content .tab-pane .dropdown-menu .active:hover { + font-size: 14px; + background-color: #f1f1f1; + } + + /*Leaflet options*/ + .leaflet-top, .leaflet-bottom { + z-index: unset !important; + } + + .leaflet-touch .leaflet-control-layers .leaflet-touch .leaflet-bar { + z-index: 10000000000 !important; + } + + .row{ + margin-left: 5px; + margin-right: 5px; + } + + .sticky-footer { + position:fixed; + bottom:0; + right:0; + left:265px; + background:white; + padding:10px; + margin-top: 15px; + border-top-width: 1px; + border-top-color: #f1f1f1; + border-top-style: solid; + }