diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md
index 8246a6e3..4d8d0b15 100644
--- a/.github/CONTRIBUTING.md
+++ b/.github/CONTRIBUTING.md
@@ -1,26 +1,26 @@
-# How to contribute
-
-Thank you for looking to contribute to our project! The following document outlines our contributors guidance.
-
-## Reporting bugs or features
-
-If you spot a bug or feature you want to report, please check first that it has not been reported as an [issue](https://github.com/dfe-analytical-services/shiny-template/issues) already.
-
-If no issue is open for your bug or feature, please [open a new one](https://github.com/dfe-analytical-services/shiny-template/issues/new)
-
-Please use the templates provided to ensure that there is sufficient detail in your reported issue.
-
-## Pull requests
-
-If you have written some code that fixes a bug or feature, please push this up in a new branch under the format "bug/issue" and open this as a new PR when it is ready to review.
-
-- Ensure the PR description clearly describes the problem and solution.
-- Include the relevant issue number if applicable.
-
-### Code styling
-
-All code is styled using the styleR package. Running the `styler::style_dir()` function before pushing up to branch ensures that your code is in line with our style, and passes the pre-commit hooks.
-
-## Any other questions
-
-If you have any other questions, please do not hesitate to contact us at jake.tufts@education.gov.uk or explore.statistics@education.gov.uk
+# How to contribute
+
+Thank you for looking to contribute to our project! The following document outlines our contributors guidance.
+
+## Reporting bugs or features
+
+If you spot a bug or feature you want to report, please check first that it has not been reported as an [issue](https://github.com/dfe-analytical-services/shiny-template/issues) already.
+
+If no issue is open for your bug or feature, please [open a new one](https://github.com/dfe-analytical-services/shiny-template/issues/new)
+
+Please use the templates provided to ensure that there is sufficient detail in your reported issue.
+
+## Pull requests
+
+If you have written some code that fixes a bug or feature, please push this up in a new branch under the format "bug/issue" and open this as a new PR when it is ready to review.
+
+- Ensure the PR description clearly describes the problem and solution.
+- Include the relevant issue number if applicable.
+
+### Code styling
+
+All code is styled using the styleR package. Running the `styler::style_dir()` function before pushing up to branch ensures that your code is in line with our style, and passes the pre-commit hooks.
+
+## Any other questions
+
+If you have any other questions, please do not hesitate to contact us at Darlington.BRIDGE@education.gov.uk or explore.statistics@education.gov.uk
diff --git a/02_dev/info_pages/dev_user_guide.R b/02_dev/info_pages/dev_user_guide.R
index a432749e..a6f9cc08 100644
--- a/02_dev/info_pages/dev_user_guide.R
+++ b/02_dev/info_pages/dev_user_guide.R
@@ -77,15 +77,8 @@ ui_dev <- function(input, output, session) {
shinyGovstyle::banner(
"beta banner",
"beta",
- paste0(
- "This Dashboard is in beta phase and we are still reviewing performance
- and reliability. ",
- "In case of slowdown or connection issues due to high demand, we have
- produced two instances of this site which can be accessed at the
- following links: ",
- "Site 1 and ",
- "Site 2."
- )
+ "This Dashboard is in beta phase and we are still reviewing performance
+ and reliability. "
),
# Start of app ============================================================
diff --git a/02_dev/la_level_page/la_dev_app.R b/02_dev/la_level_page/la_dev_app.R
index 7bc32945..ce50a270 100644
--- a/02_dev/la_level_page/la_dev_app.R
+++ b/02_dev/la_level_page/la_dev_app.R
@@ -1,645 +1,644 @@
-# Load global
-source(here::here("global.R"))
-
-# Load functions
-list.files("R/", full.names = TRUE) |>
- (\(x) {
- x[grepl("fn_", x)]
- })() |>
- purrr::walk(source)
-
-
-# UI
-ui_dev <- bslib::page_fillable(
-
- ## Custom CSS =============================================================
- shiny::includeCSS(here::here("www/dfe_shiny_gov_style.css")),
-
- # Tab header ==============================================================
- h1("Local Authority View"),
- div(
- class = "well",
- style = "overflow-y: visible;",
- bslib::layout_column_wrap(
- width = "15rem", # Minimum width for each input box before wrapping
- shiny::selectizeInput(
- inputId = "la_input",
- label = "LA:",
- choices = la_names_bds
- ),
- shiny::selectizeInput(
- inputId = "topic_input",
- label = "Topic:",
- choices = c("All topics", metric_topics),
- multiple = TRUE,
- options = list(
- maxItems = 1,
- placeholder = "No topic selected, showing all indicators.",
- plugins = list("clear_button"),
- dropdownParent = "body"
- )
- ),
- shiny::selectizeInput(
- inputId = "indicator",
- label = "Indicator:",
- choices = metric_names
- )
- ),
- # Conditional State-funded school banner
- shiny::uiOutput("state_funded_banner")
- ),
- div(
- class = "well",
- style = "overflow-y: visible;",
- bslib::card(
- bslib::card_header("Local Authority, Region and England"),
- bslib::card_body(
- shinycssloaders::withSpinner(
- reactable::reactableOutput("la_table"),
- type = 6,
- color = "#1d70b8"
- )
- )
- )
- ),
- div(
- class = "well",
- style = "overflow-y: visible;",
- bslib::card(
- bslib::card_body(
- shinycssloaders::withSpinner(
- reactable::reactableOutput("la_stats_table"),
- type = 6,
- color = "#1d70b8",
- size = 0.5,
- proxy.height = "100px"
- )
- )
- )
- ),
- div(
- class = "well",
- style = "overflow-y: visible;",
- bslib::navset_card_underline(
- id = "la_charts",
- bslib::nav_panel(
- title = "Line chart",
- bslib::card(
- bslib::card_body(
- shinycssloaders::withSpinner(
- ggiraph::girafeOutput("la_line_chart"),
- type = 6,
- color = "#1d70b8"
- )
- ),
- full_screen = TRUE
- ),
- ),
- bslib::nav_panel(
- title = "Bar chart",
- bslib::card(
- id = "la_bar_body",
- bslib::card_body(
- shinycssloaders::withSpinner(
- ggiraph::girafeOutput("la_bar_chart"),
- type = 6,
- color = "#1d70b8"
- )
- ),
- full_screen = TRUE
- )
- )
- )
- ),
- div(
- class = "well",
- style = "overflow-y: visible;",
- bslib::card(
- bslib::card_body(
- h3("Description:"),
- shinycssloaders::withSpinner(
- textOutput("description"),
- type = 6,
- color = "#1d70b8"
- ),
- h3("Methodology:"),
- shinycssloaders::withSpinner(
- uiOutput("methodology"),
- type = 6,
- color = "#1d70b8"
- ),
- div(
- # Creates a flex container where the items are centered vertically
- style = "display: flex; align-items: baseline;",
- h3("Last Updated:",
- style = "margin-right: 1rem; margin-bottom: 0.3rem;"
- ),
- shinycssloaders::withSpinner(
- textOutput("last_update"),
- type = 6,
- color = "#1d70b8"
- )
- ),
- div(
- style = "display: flex; align-items: baseline;",
- h3("Next Updated:",
- style = "margin-right: 1rem; margin-bottom: 0.3rem;"
- ),
- shinycssloaders::withSpinner(
- uiOutput("next_update"),
- type = 6,
- color = "#1d70b8"
- )
- ),
- div(
- style = "display: flex; align-items: baseline;",
- h3("Source:",
- style = "margin-right: 1rem; margin-bottom: 0.3rem;"
- ),
- shinycssloaders::withSpinner(
- uiOutput("source"),
- type = 6,
- color = "#1d70b8"
- )
- )
- )
- )
- )
-)
-
-
-# Server
-server_dev <- function(input, output, session) {
- # Input ----------------------------------
- # Using the server to power to the provider dropdown for increased speed
- shiny::observeEvent(input$topic_input,
- {
- # Save the currently selected indicator
- current_indicator <- input$indicator
-
- # Get indicator choices for selected topic
- # Include all rows if no topic is selected or "All topics" is selected
- filtered_topic_bds <- bds_metrics |>
- dplyr::filter(
- if (is.null(input$topic_input) || "All topics" %in% input$topic_input) {
- TRUE
- } else {
- .data$Topic %in% input$topic_input # Filter by selected topic(s)
- }
- ) |>
- pull_uniques("Measure")
-
- # Ensure the current indicator stays selected if it's in the new list of available indicators
- # Default to the first available indicator if the current one is no longer valid
- selected_indicator <- if (current_indicator %in% filtered_topic_bds) {
- current_indicator
- } else {
- filtered_topic_bds[1]
- }
-
- shiny::updateSelectizeInput(
- session = session,
- inputId = "indicator",
- label = "Indicator:",
- choices = filtered_topic_bds,
- selected = selected_indicator
- )
- },
- ignoreNULL = FALSE
- )
-
-
- # Main LA Level table ----------------------------------
- # Filter for selectedindicator
- # Define filtered_bds outside of observeEvent
- filtered_bds <- reactiveValues(data = NULL)
-
- observeEvent(input$indicator, {
- # Don't change the currently selected indicator if no indicator is selected
- if (is.null(input$indicator) || input$indicator == "") {
- return()
- }
-
- # Main LA Level table ----------------------------------
- # Filter for selected indicator
- filtered_bds$data <- bds_metrics |>
- dplyr::filter(
- Measure == input$indicator
- )
- })
-
- # Get decimal places for indicator selected
- indicator_dps <- reactive({
- filtered_bds$data |>
- get_indicator_dps()
- })
-
- # Long format LA data
- la_long <- reactive({
- # Filter stat neighbour for selected LA
- filtered_sn <- stat_n_la |>
- dplyr::filter(`LA Name` == input$la_input)
-
- # Statistical Neighbours
- la_sns <- filtered_sn |>
- pull_uniques("LA Name_sn")
-
- # LA region
- la_region <- filtered_sn |>
- pull_uniques("GOReg")
-
- # Determine London region to use
- la_region_ldn_clean <- clean_ldn_region(
- la_region,
- filtered_bds$data
- )
-
- # Then filter for selected LA, region, stat neighbours and relevant national
- la_filtered_bds <- filtered_bds$data |>
- dplyr::filter(
- `LA and Regions` %in% c(input$la_input, la_region_ldn_clean, la_sns, "England")
- )
-
- # SN average
- sn_avg <- la_filtered_bds |>
- dplyr::filter(`LA and Regions` %in% la_sns) |>
- dplyr::summarise(
- values_num = dplyr::na_if(mean(values_num, na.rm = TRUE), NaN),
- .by = c("Years", "Years_num")
- ) |>
- dplyr::mutate(
- "LA Number" = "-",
- "LA and Regions" = "Statistical Neighbours",
- .before = "Years"
- )
-
- # LA levels long
- la_filtered_bds |>
- dplyr::filter(`LA and Regions` %notin% c(la_sns)) |>
- dplyr::select(`LA Number`, `LA and Regions`, Years, Years_num, values_num) |>
- dplyr::bind_rows(sn_avg) |>
- dplyr::mutate(
- `LA and Regions` = factor(
- `LA and Regions`,
- levels = c(
- input$la_input, la_region_ldn_clean,
- "Statistical Neighbours", "England"
- )
- )
- )
- })
-
- # Difference between last two years
- la_diff <- reactive({
- la_long() |>
- dplyr::group_by(`LA and Regions`) |>
- dplyr::arrange(`LA and Regions`, desc(Years)) |>
- dplyr::mutate(
- values_num = dplyr::lag(values_num) - values_num,
- Years = "Change from previous year"
- ) |>
- dplyr::filter(dplyr::row_number() == 2)
- })
-
- # Build Main LA Level table
- la_table <- shiny::reactive({
- # Join difference and pivot wider to recreate LAIT table
- la_long() |>
- dplyr::bind_rows(la_diff()) |>
- tidyr::pivot_wider(
- id_cols = c("LA Number", "LA and Regions"),
- names_from = Years,
- values_from = values_num
- ) |>
- dplyr::arrange(`LA and Regions`)
- })
-
-
- # Stet funded school banner (appears for certain indicators)
- output$state_funded_banner <- renderUI({
- # Get whether state-funded idnicator
- state_funded <- filtered_bds$data |>
- pull_uniques("state_funded_flag") |>
- (\(x) !is.na(x))()
-
- # Render banner if state-funded
- if (state_funded) {
- tagList(
- br(),
- shinyGovstyle::noti_banner(
- inputId = "notId",
- title_txt = "Note",
- body_txt = "Data includes only State-funded Schools."
- )
- )
- }
- })
-
- output$la_table <- reactable::renderReactable({
- dfe_reactable(
- la_table(),
- columns = utils::modifyList(
- format_num_reactable_cols(
- la_table(),
- get_indicator_dps(filtered_bds$data),
- num_exclude = "LA Number"
- ),
- set_custom_default_col_widths()
- ),
- rowStyle = function(index) {
- highlight_selected_row(index, la_table(), input$la_input)
- }
- )
- })
-
-
- # Stats LA Level table ----------------------------------
- la_stats_table <- shiny::reactive({
- # Extract change from prev year (from LA table)
- la_change_prev <- la_diff() |>
- filter_la_regions(input$la_input, pull_col = "values_num")
-
- # Set the trend value
- la_trend <- as.numeric(la_change_prev)
-
- # Get polarity of indicator
- la_indicator_polarity <- filtered_bds$data |>
- pull_uniques("Polarity")
-
- # Get latest rank, ties are set to min & NA vals to NA rank
- la_rank <- filtered_bds$data |>
- filter_la_regions(la_names_bds, latest = TRUE) |>
- calculate_rank(la_indicator_polarity) |>
- filter_la_regions(input$la_input, pull_col = "rank")
-
- # Calculate quartile bands for indicator
- la_quartile_bands <- filtered_bds$data |>
- filter_la_regions(la_names_bds, latest = TRUE, pull_col = "values_num") |>
- quantile(na.rm = TRUE)
-
- # Extracting LA latest value
- la_indicator_val <- filtered_bds$data |>
- filter_la_regions(input$la_input, latest = TRUE, pull_col = "values_num")
-
- # Boolean as to whether to include Quartile Banding
- no_show_qb <- input$indicator %in% no_qb_indicators
-
- # Calculating which quartile this value sits in
- la_quartile <- calculate_quartile_band(
- la_indicator_val,
- la_quartile_bands,
- la_indicator_polarity
- )
-
- # Build stats LA Level table
- la_stats_table <- build_la_stats_table(
- la_diff(),
- input$la_input,
- la_trend,
- la_change_prev,
- la_rank,
- la_quartile,
- la_quartile_bands,
- get_indicator_dps(filtered_bds$data),
- la_indicator_polarity,
- no_show_qb
- )
-
- la_stats_table
- })
-
- output$la_stats_table <- reactable::renderReactable({
- dfe_reactable(
- la_stats_table(),
- columns = modifyList(
- # Create the reactable with specific column alignments
- format_num_reactable_cols(
- la_stats_table(),
- get_indicator_dps(filtered_bds$data),
- num_exclude = "LA Number",
- categorical = c(
- "Trend", "Quartile Banding", "Latest National Rank",
- "A", "B",
- "C", "D"
- )
- ),
- # Style Quartile Banding column with colour
- list(
- set_custom_default_col_widths(),
- Trend = reactable::colDef(
- header = add_tooltip_to_reactcol(
- "Trend",
- "Based on change from previous year"
- ),
- cell = trend_icon_renderer,
- style = function(value) {
- get_trend_colour(value, la_stats_table()$Polarity[1])
- }
- ),
- `Quartile Banding` = reactable::colDef(
- style = function(value, index) {
- quartile_banding_col_def(la_stats_table()[index, ])
- }
- ),
- `Latest National Rank` = reactable::colDef(
- header = add_tooltip_to_reactcol(
- "Latest National Rank",
- "Rank 1 is always best/top"
- )
- ),
- Polarity = reactable::colDef(show = FALSE)
- )
- )
- )
- })
-
-
- # LA Level line chart plot ----------------------------------
- la_line_chart <- reactive({
- # Generate the covid plot data if add_covid_plot is TRUE
- covid_plot <- calculate_covid_plot(
- la_long(),
- covid_affected_data,
- input$indicator,
- "line"
- )
-
- # Build plot
- la_line_chart <- la_long() |>
- # Set geog orders so selected LA is on top of plot
- reorder_la_regions(reverse = TRUE) |>
- ggplot2::ggplot() +
- ggiraph::geom_line_interactive(
- ggplot2::aes(
- x = Years_num,
- y = values_num,
- color = `LA and Regions`,
- data_id = `LA and Regions`
- ),
- na.rm = TRUE,
- linewidth = 1
- ) +
- # Only show point data where line won't appear (NAs)
- ggplot2::geom_point(
- data = subset(create_show_point(
- la_long(),
- covid_affected_data,
- input$indicator
- ), show_point),
- ggplot2::aes(
- x = Years_num,
- y = values_num,
- color = `LA and Regions`
- ),
- shape = 15,
- size = 1,
- na.rm = TRUE
- ) +
- # Add COVID plot if indicator affected
- add_covid_elements(covid_plot) +
- format_axes(la_long()) +
- set_plot_colours(la_long(), focus_group = input$la_input) +
- set_plot_labs(filtered_bds$data) +
- custom_theme() +
- # Revert order of the legend so goes from right to left
- ggplot2::guides(color = ggplot2::guide_legend(reverse = TRUE))
-
- # Creating vertical geoms to make vertical hover tooltip
- vertical_hover <- lapply(
- get_years(la_long()),
- tooltip_vlines,
- la_long(),
- indicator_dps(),
- input$la_input
- )
-
- # Plotting interactive graph
- ggiraph::girafe(
- ggobj = (la_line_chart + vertical_hover),
- width_svg = 8.5,
- options = generic_ggiraph_options(
- opts_hover(
- css = "stroke-dasharray:5,5;stroke:black;stroke-width:2px;"
- )
- ),
- fonts = list(sans = "Arial")
- )
- })
-
- output$la_line_chart <- ggiraph::renderGirafe({
- la_line_chart()
- })
-
-
- # LA Level bar plot ----------------------------------
- la_bar_chart <- reactive({
- # Generate the covid plot data if add_covid_plot is TRUE
- covid_plot <- calculate_covid_plot(
- la_long(),
- covid_affected_data,
- input$indicator,
- "bar"
- )
-
- # Build plot
- la_bar_chart <- la_long() |>
- ggplot2::ggplot() +
- ggiraph::geom_col_interactive(
- ggplot2::aes(
- x = Years_num,
- y = values_num,
- fill = `LA and Regions`,
- tooltip = tooltip_bar(la_long(), indicator_dps(), input$la_input),
- data_id = `LA and Regions`
- ),
- position = "dodge",
- width = 0.6,
- na.rm = TRUE,
- colour = "black"
- ) +
- # Add COVID plot if indicator affected
- add_covid_elements(covid_plot) +
- format_axes(la_long()) +
- set_plot_colours(la_long(), "fill", input$la_input) +
- set_plot_labs(filtered_bds$data) +
- custom_theme()
-
- # Plotting interactive graph
- ggiraph::girafe(
- ggobj = la_bar_chart,
- width_svg = 8.5,
- options = generic_ggiraph_options(
- opts_hover(
- css = "stroke-dasharray:5,5;stroke:black;stroke-width:2px;"
- )
- ),
- fonts = list(sans = "Arial")
- )
- })
-
-
- output$la_bar_chart <- ggiraph::renderGirafe({
- la_bar_chart()
- })
-
-
- # LA Metadata ----------------------------------
- # Reactive values to store previous data
- previous_metadata <- reactiveValues(
- description = NULL,
- methodology = NULL,
- last_update = NULL,
- next_update = NULL,
- source = NULL
- )
-
- # Outputs using the helper function
- output$description <- renderText({
- update_and_fetch_metadata(
- input$indicator,
- "Description",
- previous_metadata,
- "description"
- )
- })
-
- output$methodology <- renderUI({
- update_and_fetch_metadata(
- input$indicator,
- "Methodology",
- previous_metadata,
- "methodology"
- )
- })
-
- output$last_update <- renderText({
- update_and_fetch_metadata(
- input$indicator,
- "Last Update",
- previous_metadata,
- "last_update"
- )
- })
-
- output$next_update <- renderUI({
- update_and_fetch_metadata(
- input$indicator,
- "Next Update",
- previous_metadata,
- "next_update"
- )
- })
-
- output$source <- renderUI({
- hyperlink <- update_and_fetch_metadata(
- input$indicator,
- "Hyperlink(s)",
- previous_metadata,
- "source"
- )
- dfeshiny::external_link(href = hyperlink, link_text = input$indicator)
- })
-}
-
-# App
-shinyApp(ui_dev, server_dev)
+# Load global
+source(here::here("global.R"))
+
+# Load functions
+list.files("R/", full.names = TRUE) |>
+ (\(x) {
+ x[grepl("fn_", x)]
+ })() |>
+ purrr::walk(source)
+
+
+# UI
+ui_dev <- bslib::page_fillable(
+
+ ## Custom CSS =============================================================
+ shiny::includeCSS(here::here("www/dfe_shiny_gov_style.css")),
+
+ # Tab header ==============================================================
+ h1("Local Authority View"),
+ div(
+ class = "well",
+ style = "overflow-y: visible;",
+ bslib::layout_column_wrap(
+ width = "15rem", # Minimum width for each input box before wrapping
+ shiny::selectizeInput(
+ inputId = "la_input",
+ label = "LA:",
+ choices = la_names_bds
+ ),
+ shiny::selectizeInput(
+ inputId = "topic_input",
+ label = "Topic:",
+ choices = c("All topics", metric_topics),
+ multiple = TRUE,
+ options = list(
+ maxItems = 1,
+ placeholder = "No topic selected, showing all indicators.",
+ plugins = list("clear_button"),
+ dropdownParent = "body"
+ )
+ ),
+ shiny::selectizeInput(
+ inputId = "indicator",
+ label = "Indicator:",
+ choices = metric_names
+ )
+ ),
+ # Conditional State-funded school banner
+ shiny::uiOutput("state_funded_banner")
+ ),
+ div(
+ class = "well",
+ style = "overflow-y: visible;",
+ bslib::card(
+ bslib::card_body(
+ shinycssloaders::withSpinner(
+ reactable::reactableOutput("la_table"),
+ type = 6,
+ color = "#1d70b8"
+ )
+ )
+ )
+ ),
+ div(
+ class = "well",
+ style = "overflow-y: visible;",
+ bslib::card(
+ bslib::card_body(
+ shinycssloaders::withSpinner(
+ reactable::reactableOutput("la_stats_table"),
+ type = 6,
+ color = "#1d70b8",
+ size = 0.5,
+ proxy.height = "100px"
+ )
+ )
+ )
+ ),
+ div(
+ class = "well",
+ style = "overflow-y: visible;",
+ bslib::navset_card_underline(
+ id = "la_charts",
+ bslib::nav_panel(
+ title = "Line chart",
+ bslib::card(
+ bslib::card_body(
+ shinycssloaders::withSpinner(
+ ggiraph::girafeOutput("la_line_chart"),
+ type = 6,
+ color = "#1d70b8"
+ )
+ ),
+ full_screen = TRUE
+ ),
+ ),
+ bslib::nav_panel(
+ title = "Bar chart",
+ bslib::card(
+ id = "la_bar_body",
+ bslib::card_body(
+ shinycssloaders::withSpinner(
+ ggiraph::girafeOutput("la_bar_chart"),
+ type = 6,
+ color = "#1d70b8"
+ )
+ ),
+ full_screen = TRUE
+ )
+ )
+ )
+ ),
+ div(
+ class = "well",
+ style = "overflow-y: visible;",
+ bslib::card(
+ bslib::card_body(
+ h3("Description:"),
+ shinycssloaders::withSpinner(
+ textOutput("description"),
+ type = 6,
+ color = "#1d70b8"
+ ),
+ h3("Methodology:"),
+ shinycssloaders::withSpinner(
+ uiOutput("methodology"),
+ type = 6,
+ color = "#1d70b8"
+ ),
+ div(
+ # Creates a flex container where the items are centered vertically
+ style = "display: flex; align-items: baseline;",
+ h3("Last Updated:",
+ style = "margin-right: 1rem; margin-bottom: 0.3rem;"
+ ),
+ shinycssloaders::withSpinner(
+ textOutput("last_update"),
+ type = 6,
+ color = "#1d70b8"
+ )
+ ),
+ div(
+ style = "display: flex; align-items: baseline;",
+ h3("Next Updated:",
+ style = "margin-right: 1rem; margin-bottom: 0.3rem;"
+ ),
+ shinycssloaders::withSpinner(
+ uiOutput("next_update"),
+ type = 6,
+ color = "#1d70b8"
+ )
+ ),
+ div(
+ style = "display: flex; align-items: baseline;",
+ h3("Source:",
+ style = "margin-right: 1rem; margin-bottom: 0.3rem;"
+ ),
+ shinycssloaders::withSpinner(
+ uiOutput("source"),
+ type = 6,
+ color = "#1d70b8"
+ )
+ )
+ )
+ )
+ )
+)
+
+
+# Server
+server_dev <- function(input, output, session) {
+ # Input ----------------------------------
+ # Using the server to power to the provider dropdown for increased speed
+ shiny::observeEvent(input$topic_input,
+ {
+ # Save the currently selected indicator
+ current_indicator <- input$indicator
+
+ # Get indicator choices for selected topic
+ # Include all rows if no topic is selected or "All topics" is selected
+ filtered_topic_bds <- bds_metrics |>
+ dplyr::filter(
+ if (is.null(input$topic_input) || "All topics" %in% input$topic_input) {
+ TRUE
+ } else {
+ .data$Topic %in% input$topic_input # Filter by selected topic(s)
+ }
+ ) |>
+ pull_uniques("Measure")
+
+ # Ensure the current indicator stays selected if it's in the new list of available indicators
+ # Default to the first available indicator if the current one is no longer valid
+ selected_indicator <- if (current_indicator %in% filtered_topic_bds) {
+ current_indicator
+ } else {
+ filtered_topic_bds[1]
+ }
+
+ shiny::updateSelectizeInput(
+ session = session,
+ inputId = "indicator",
+ label = "Indicator:",
+ choices = filtered_topic_bds,
+ selected = selected_indicator
+ )
+ },
+ ignoreNULL = FALSE
+ )
+
+
+ # Main LA Level table ----------------------------------
+ # Filter for selectedindicator
+ # Define filtered_bds outside of observeEvent
+ filtered_bds <- reactiveValues(data = NULL)
+
+ observeEvent(input$indicator, {
+ # Don't change the currently selected indicator if no indicator is selected
+ if (is.null(input$indicator) || input$indicator == "") {
+ return()
+ }
+
+ # Main LA Level table ----------------------------------
+ # Filter for selected indicator
+ filtered_bds$data <- bds_metrics |>
+ dplyr::filter(
+ Measure == input$indicator
+ )
+ })
+
+ # Get decimal places for indicator selected
+ indicator_dps <- reactive({
+ filtered_bds$data |>
+ get_indicator_dps()
+ })
+
+ # Long format LA data
+ la_long <- reactive({
+ # Filter stat neighbour for selected LA
+ filtered_sn <- stat_n_la |>
+ dplyr::filter(`LA Name` == input$la_input)
+
+ # Statistical Neighbours
+ la_sns <- filtered_sn |>
+ pull_uniques("LA Name_sn")
+
+ # LA region
+ la_region <- filtered_sn |>
+ pull_uniques("GOReg")
+
+ # Determine London region to use
+ la_region_ldn_clean <- clean_ldn_region(
+ la_region,
+ filtered_bds$data
+ )
+
+ # Then filter for selected LA, region, stat neighbours and relevant national
+ la_filtered_bds <- filtered_bds$data |>
+ dplyr::filter(
+ `LA and Regions` %in% c(input$la_input, la_region_ldn_clean, la_sns, "England")
+ )
+
+ # SN average
+ sn_avg <- la_filtered_bds |>
+ dplyr::filter(`LA and Regions` %in% la_sns) |>
+ dplyr::summarise(
+ values_num = dplyr::na_if(mean(values_num, na.rm = TRUE), NaN),
+ .by = c("Years", "Years_num")
+ ) |>
+ dplyr::mutate(
+ "LA Number" = "-",
+ "LA and Regions" = "Statistical Neighbours",
+ .before = "Years"
+ )
+
+ # LA levels long
+ la_filtered_bds |>
+ dplyr::filter(`LA and Regions` %notin% c(la_sns)) |>
+ dplyr::select(`LA Number`, `LA and Regions`, Years, Years_num, values_num) |>
+ dplyr::bind_rows(sn_avg) |>
+ dplyr::mutate(
+ `LA and Regions` = factor(
+ `LA and Regions`,
+ levels = c(
+ input$la_input, la_region_ldn_clean,
+ "Statistical Neighbours", "England"
+ )
+ )
+ )
+ })
+
+ # Difference between last two years
+ la_diff <- reactive({
+ la_long() |>
+ dplyr::group_by(`LA and Regions`) |>
+ dplyr::arrange(`LA and Regions`, desc(Years)) |>
+ dplyr::mutate(
+ values_num = dplyr::lag(values_num) - values_num,
+ Years = "Change from previous year"
+ ) |>
+ dplyr::filter(dplyr::row_number() == 2)
+ })
+
+ # Build Main LA Level table
+ la_table <- shiny::reactive({
+ # Join difference and pivot wider to recreate LAIT table
+ la_long() |>
+ dplyr::bind_rows(la_diff()) |>
+ tidyr::pivot_wider(
+ id_cols = c("LA Number", "LA and Regions"),
+ names_from = Years,
+ values_from = values_num
+ ) |>
+ dplyr::arrange(`LA and Regions`)
+ })
+
+
+ # Stet funded school banner (appears for certain indicators)
+ output$state_funded_banner <- renderUI({
+ # Get whether state-funded idnicator
+ state_funded <- filtered_bds$data |>
+ pull_uniques("state_funded_flag") |>
+ (\(x) !is.na(x))()
+
+ # Render banner if state-funded
+ if (state_funded) {
+ tagList(
+ br(),
+ shinyGovstyle::noti_banner(
+ inputId = "notId",
+ title_txt = "Note",
+ body_txt = "Data includes only State-funded Schools."
+ )
+ )
+ }
+ })
+
+ output$la_table <- reactable::renderReactable({
+ dfe_reactable(
+ la_table(),
+ columns = utils::modifyList(
+ format_num_reactable_cols(
+ la_table(),
+ get_indicator_dps(filtered_bds$data),
+ num_exclude = "LA Number"
+ ),
+ set_custom_default_col_widths()
+ ),
+ rowStyle = function(index) {
+ highlight_selected_row(index, la_table(), input$la_input)
+ }
+ )
+ })
+
+
+ # Stats LA Level table ----------------------------------
+ la_stats_table <- shiny::reactive({
+ # Extract change from prev year (from LA table)
+ la_change_prev <- la_diff() |>
+ filter_la_regions(input$la_input, pull_col = "values_num")
+
+ # Set the trend value
+ la_trend <- as.numeric(la_change_prev)
+
+ # Get polarity of indicator
+ la_indicator_polarity <- filtered_bds$data |>
+ pull_uniques("Polarity")
+
+ # Get latest rank, ties are set to min & NA vals to NA rank
+ la_rank <- filtered_bds$data |>
+ filter_la_regions(la_names_bds, latest = TRUE) |>
+ calculate_rank(la_indicator_polarity) |>
+ filter_la_regions(input$la_input, pull_col = "rank")
+
+ # Calculate quartile bands for indicator
+ la_quartile_bands <- filtered_bds$data |>
+ filter_la_regions(la_names_bds, latest = TRUE, pull_col = "values_num") |>
+ quantile(na.rm = TRUE)
+
+ # Extracting LA latest value
+ la_indicator_val <- filtered_bds$data |>
+ filter_la_regions(input$la_input, latest = TRUE, pull_col = "values_num")
+
+ # Boolean as to whether to include Quartile Banding
+ no_show_qb <- input$indicator %in% no_qb_indicators
+
+ # Calculating which quartile this value sits in
+ la_quartile <- calculate_quartile_band(
+ la_indicator_val,
+ la_quartile_bands,
+ la_indicator_polarity
+ )
+
+ # Build stats LA Level table
+ la_stats_table <- build_la_stats_table(
+ la_diff(),
+ input$la_input,
+ la_trend,
+ la_change_prev,
+ la_rank,
+ la_quartile,
+ la_quartile_bands,
+ get_indicator_dps(filtered_bds$data),
+ la_indicator_polarity,
+ no_show_qb
+ )
+
+ la_stats_table
+ })
+
+ output$la_stats_table <- reactable::renderReactable({
+ dfe_reactable(
+ la_stats_table(),
+ columns = modifyList(
+ # Create the reactable with specific column alignments
+ format_num_reactable_cols(
+ la_stats_table(),
+ get_indicator_dps(filtered_bds$data),
+ num_exclude = "LA Number",
+ categorical = c(
+ "Trend", "Quartile Banding", "Latest National Rank",
+ "A", "B",
+ "C", "D"
+ )
+ ),
+ # Style Quartile Banding column with colour
+ list(
+ set_custom_default_col_widths(),
+ Trend = reactable::colDef(
+ header = add_tooltip_to_reactcol(
+ "Trend",
+ "Based on change from previous year"
+ ),
+ cell = trend_icon_renderer,
+ style = function(value) {
+ get_trend_colour(value, la_stats_table()$Polarity[1])
+ }
+ ),
+ `Quartile Banding` = reactable::colDef(
+ style = function(value, index) {
+ quartile_banding_col_def(la_stats_table()[index, ])
+ }
+ ),
+ `Latest National Rank` = reactable::colDef(
+ header = add_tooltip_to_reactcol(
+ "Latest National Rank",
+ "Rank 1 is always best/top"
+ )
+ ),
+ Polarity = reactable::colDef(show = FALSE)
+ )
+ )
+ )
+ })
+
+
+ # LA Level line chart plot ----------------------------------
+ la_line_chart <- reactive({
+ # Generate the covid plot data if add_covid_plot is TRUE
+ covid_plot <- calculate_covid_plot(
+ la_long(),
+ covid_affected_data,
+ input$indicator,
+ "line"
+ )
+
+ # Build plot
+ la_line_chart <- la_long() |>
+ # Set geog orders so selected LA is on top of plot
+ reorder_la_regions(reverse = TRUE) |>
+ ggplot2::ggplot() +
+ ggiraph::geom_line_interactive(
+ ggplot2::aes(
+ x = Years_num,
+ y = values_num,
+ color = `LA and Regions`,
+ data_id = `LA and Regions`
+ ),
+ na.rm = TRUE,
+ linewidth = 1
+ ) +
+ # Only show point data where line won't appear (NAs)
+ ggplot2::geom_point(
+ data = subset(create_show_point(
+ la_long(),
+ covid_affected_data,
+ input$indicator
+ ), show_point),
+ ggplot2::aes(
+ x = Years_num,
+ y = values_num,
+ color = `LA and Regions`
+ ),
+ shape = 15,
+ size = 1,
+ na.rm = TRUE
+ ) +
+ # Add COVID plot if indicator affected
+ add_covid_elements(covid_plot) +
+ format_axes(la_long()) +
+ set_plot_colours(la_long(), focus_group = input$la_input) +
+ set_plot_labs(filtered_bds$data) +
+ custom_theme() +
+ # Revert order of the legend so goes from right to left
+ ggplot2::guides(color = ggplot2::guide_legend(reverse = TRUE))
+
+ # Creating vertical geoms to make vertical hover tooltip
+ vertical_hover <- lapply(
+ get_years(la_long()),
+ tooltip_vlines,
+ la_long(),
+ indicator_dps(),
+ input$la_input
+ )
+
+ # Plotting interactive graph
+ ggiraph::girafe(
+ ggobj = (la_line_chart + vertical_hover),
+ width_svg = 8.5,
+ options = generic_ggiraph_options(
+ opts_hover(
+ css = "stroke-dasharray:5,5;stroke:black;stroke-width:2px;"
+ )
+ ),
+ fonts = list(sans = "Arial")
+ )
+ })
+
+ output$la_line_chart <- ggiraph::renderGirafe({
+ la_line_chart()
+ })
+
+
+ # LA Level bar plot ----------------------------------
+ la_bar_chart <- reactive({
+ # Generate the covid plot data if add_covid_plot is TRUE
+ covid_plot <- calculate_covid_plot(
+ la_long(),
+ covid_affected_data,
+ input$indicator,
+ "bar"
+ )
+
+ # Build plot
+ la_bar_chart <- la_long() |>
+ ggplot2::ggplot() +
+ ggiraph::geom_col_interactive(
+ ggplot2::aes(
+ x = Years_num,
+ y = values_num,
+ fill = `LA and Regions`,
+ tooltip = tooltip_bar(la_long(), indicator_dps(), input$la_input),
+ data_id = `LA and Regions`
+ ),
+ position = "dodge",
+ width = 0.6,
+ na.rm = TRUE,
+ colour = "black"
+ ) +
+ # Add COVID plot if indicator affected
+ add_covid_elements(covid_plot) +
+ format_axes(la_long()) +
+ set_plot_colours(la_long(), "fill", input$la_input) +
+ set_plot_labs(filtered_bds$data) +
+ custom_theme()
+
+ # Plotting interactive graph
+ ggiraph::girafe(
+ ggobj = la_bar_chart,
+ width_svg = 8.5,
+ options = generic_ggiraph_options(
+ opts_hover(
+ css = "stroke-dasharray:5,5;stroke:black;stroke-width:2px;"
+ )
+ ),
+ fonts = list(sans = "Arial")
+ )
+ })
+
+
+ output$la_bar_chart <- ggiraph::renderGirafe({
+ la_bar_chart()
+ })
+
+
+ # LA Metadata ----------------------------------
+ # Reactive values to store previous data
+ previous_metadata <- reactiveValues(
+ description = NULL,
+ methodology = NULL,
+ last_update = NULL,
+ next_update = NULL,
+ source = NULL
+ )
+
+ # Outputs using the helper function
+ output$description <- renderText({
+ update_and_fetch_metadata(
+ input$indicator,
+ "Description",
+ previous_metadata,
+ "description"
+ )
+ })
+
+ output$methodology <- renderUI({
+ update_and_fetch_metadata(
+ input$indicator,
+ "Methodology",
+ previous_metadata,
+ "methodology"
+ )
+ })
+
+ output$last_update <- renderText({
+ update_and_fetch_metadata(
+ input$indicator,
+ "Last Update",
+ previous_metadata,
+ "last_update"
+ )
+ })
+
+ output$next_update <- renderUI({
+ update_and_fetch_metadata(
+ input$indicator,
+ "Next Update",
+ previous_metadata,
+ "next_update"
+ )
+ })
+
+ output$source <- renderUI({
+ hyperlink <- update_and_fetch_metadata(
+ input$indicator,
+ "Hyperlink(s)",
+ previous_metadata,
+ "source"
+ )
+ dfeshiny::external_link(href = hyperlink, link_text = input$indicator)
+ })
+}
+
+# App
+shinyApp(ui_dev, server_dev)
diff --git a/02_dev/la_level_page/la_dev_app_mod.R b/02_dev/la_level_page/la_dev_app_mod.R
index c5bbb4f6..014fe2d7 100644
--- a/02_dev/la_level_page/la_dev_app_mod.R
+++ b/02_dev/la_level_page/la_dev_app_mod.R
@@ -38,7 +38,7 @@ ui_mod <- bslib::page_fillable(
div(
class = "well",
style = "overflow-y: visible;",
- bslib::navset_card_underline(
+ bslib::navset_card_tab(
id = "la_charts",
LA_LineChartUI("la_line_chart"),
LA_BarChartUI("la_bar_chart")
diff --git a/R/fn_analysis.R b/R/fn_analysis.R
index 6a3d3f48..cfb6b0d1 100644
--- a/R/fn_analysis.R
+++ b/R/fn_analysis.R
@@ -269,10 +269,11 @@ calculate_rank <- function(filtered_data, indicator_polarity) {
#' # Assuming `df` is your data frame and `la_names_vec` is a vector of LA names
#' filtered_data <- filter_la_data_all_la(df, la_names_vec)
#'
-filter_la_data_all_la <- function(data, la_names) {
+filter_la_data_all_la <- function(data, la_names, geog_colname) {
data |>
dplyr::filter(`LA and Regions` %in% la_names) |>
- dplyr::arrange(`LA and Regions`)
+ dplyr::arrange(`LA and Regions`) |>
+ dplyr::rename("LA" = `LA and Regions`)
}
@@ -310,7 +311,8 @@ filter_region_data_all_la <- function(data, la_names) {
))) == 0)
) |>
dplyr::mutate(Rank = "") |>
- dplyr::arrange(`LA Number`)
+ dplyr::arrange(`LA Number`) |>
+ dplyr::rename("Region" = `LA and Regions`)
}
diff --git a/R/fn_helper_functions.R b/R/fn_helper_functions.R
index 0fd87b01..8c7a5917 100644
--- a/R/fn_helper_functions.R
+++ b/R/fn_helper_functions.R
@@ -684,11 +684,11 @@ add_line_breaks <- function(text, max_length = 20) {
#' # Wrap a plot with a larger spinner
#' with_gov_spinner(plotOutput("la_plot"), size = 2)
#'
-with_gov_spinner <- function(ui_element, spinner_type = 6, size = 1) {
+with_gov_spinner <- function(ui_element, spinner_type = 6, size = 1, color = "#1d70b8") {
shinycssloaders::withSpinner(
ui_element,
type = spinner_type,
- color = "#1d70b8",
+ color = color,
size = size,
proxy.height = paste0(250 * size, "px")
)
diff --git a/R/fn_load_data.R b/R/fn_load_data.R
index b3c23e27..e06b0e07 100644
--- a/R/fn_load_data.R
+++ b/R/fn_load_data.R
@@ -1,322 +1,358 @@
-#' Shared Folder Path
-#'
-#' This variable holds the path to the 'LAIT - modernisation' teams channel folder.
-#' The folder is synchronised and located in the user's local system.
-#'
-shared_folder <- paste0(
- r"(C:\Users\jtufts\Department for Education\LAIT modernisation - General)",
- r"(\LAIT Modernisation 2024\Information for App Development)"
-)
-
-
-#' Clean SNP Column Names
-#'
-#' This function cleans the column names of a given data frame. Specifically, it adds numbers to 'SNP' columns
-#' based on the numbers extracted from 'SN' columns.
-#'
-#' @param data A data frame that contains columns with names starting with 'SN' and 'SNP'.
-#'
-#' @return A data frame with cleaned column names.
-#'
-#' @examples
-#' \dontrun{
-#' data <- data.frame(SN1 = c(1, 2), SNP = c(3, 4), SN2 = c(5, 6), SNP = c(7, 8))
-#' clean_snp_colnames(data)
-#' }
-#'
-#' @export
-clean_snp_colnames <- function(data) {
- col_names <- colnames(data)
-
- # Logical vectors to identify "SN" and "SNP" columns
- sn_cols <- which(grepl("^SN\\d+$", col_names))
- snp_cols <- which(grepl("^SNP", col_names))
-
- if (length(sn_cols) < 1) {
- stop(
- paste0(
- "SN columns do not seem to be in the right format e.g., SNx",
- "where x is a number"
- )
- )
- }
-
- if (length(snp_cols) < 1) {
- stop(
- paste0(
- "SNP columns do not seem to be in the right format e.g., SNPx",
- "where x can be anything"
- )
- )
- }
-
- # Extract the numbers from "SN" columns
- sn_numbers <- gsub("^SN", "", col_names[sn_cols])
-
- # Clean column names vector
- clean_col_names <- col_names
-
- # Assign new names to "SNP" columns using extracted "SN" column numbers
- clean_col_names[snp_cols] <- paste0("SNP", sn_numbers)
-
- # Assign the new column names to the dataframe
- colnames(data) <- clean_col_names
-
- data
-}
-
-
-#' Create Measure Key
-#'
-#' This function creates a new column 'measure_key' in the given data frame. The 'measure_key' is created by
-#' concatenating the 'Topic' and 'Measure_short' columns, replacing spaces with underscores, and converting
-#' the resulting string to lowercase.
-#'
-#' @param data A data frame that contains the columns 'Topic' and 'Measure_short'.
-#'
-#' @return A data frame with an additional 'measure_key' column.
-#'
-#' @examples
-#' \dontrun{
-#' data <- data.frame(Topic = c("Topic1", "Topic2"), Measure_short = c("Measure1", "Measure2"))
-#' create_measure_key(data)
-#' }
-#'
-#' @export
-create_measure_key <- function(data) {
- data |>
- dplyr::mutate(
- measure_key = tolower(gsub(" ", "_", paste(Topic, Measure_short))),
- .after = Measure_short
- )
-}
-
-
-#' Generate Downloadable File Based on Data and File Type
-#'
-#' This function generates a temporary file for download based on the provided
-#' data and file type. Supported file types include CSV, XLSX, PNG, and HTML.
-#' The function writes the data to the corresponding file format, and returns
-#' the file path for download.
-#'
-#' @param data A dataset or object to be saved, which can be a data frame or
-#' a reactive list containing specific data for charts or widgets.
-#' @param file_type A string specifying the desired file type for download.
-#' Supported types are: "csv", "xlsx", "png", and "html". This string
-#' is matched case-insensitively.
-#'
-#' @return A character string representing the path to the generated file.
-#' The file will be saved temporarily with the appropriate extension
-#' based on the provided `file_type`.
-#'
-#' @details
-#' The function handles multiple file types as follows:
-#' - For `"csv"`, it writes the data to a CSV file without row names.
-#' - For `"xlsx"`, it saves the data as an Excel file with auto column widths.
-#' - For `"png"`, it saves a ggplot object to a PNG file with specified size.
-#' - For `"html"`, it saves an HTML widget to an HTML file.
-#' If an unsupported file type is provided, the function returns an error.
-#'
-#' @examples
-#' \dontrun{
-#' # Save a data frame as a CSV file
-#' generate_download_file(mtcars, "csv")
-#'
-#' # Save a ggplot chart as PNG
-#' plot_data <- list(png = ggplot(mtcars, aes(mpg, wt)) +
-#' geom_point())
-#' generate_download_file(plot_data, "png")
-#' }
-#'
-generate_download_file <- function(data, file_type, svg_width = 8.5) {
- out <- tempfile(fileext = dplyr::case_when(
- grepl("csv", file_type, ignore.case = TRUE) ~ ".csv",
- grepl("xlsx", file_type, ignore.case = TRUE) ~ ".xlsx",
- grepl("svg", file_type, ignore.case = TRUE) ~ ".svg",
- grepl("html", file_type, ignore.case = TRUE) ~ ".html",
- TRUE ~ "Error"
- ))
-
- if (grepl("csv", file_type, ignore.case = TRUE)) {
- write.csv(data, file = out, row.names = FALSE)
- } else if (grepl("xlsx", file_type, ignore.case = TRUE)) {
- openxlsx::write.xlsx(data, file = out, colWidths = "Auto")
- } else if (grepl("svg", file_type, ignore.case = TRUE)) {
- ggplot2::ggsave(filename = out, plot = data, width = svg_width, height = 6)
- } else if (grepl("html", file_type, ignore.case = TRUE)) {
- htmlwidgets::saveWidget(widget = data, file = out)
- }
-
- out
-}
-
-
-#' Create Download Handler for Shiny Application
-#'
-#' This function creates a `downloadHandler` for use in a Shiny app, allowing
-#' users to download a file in the specified format. The file name is generated
-#' dynamically based on the provided extension input and table name prefix.
-#'
-#' @param export_file A string representing the file path of the file to be
-#' downloaded. This should be a pre-generated file ready for download.
-#' @param ext_input A reactive expression that returns the file type selected
-#' by the user. Supported types are: "xlsx", "csv", "png", and "html".
-#' The file extension is matched case-insensitively.
-#' @param table_name_prefix A reactive expression returning a vector of strings
-#' that represent parts of the file name. These are concatenated with
-#' hyphens (`-`) to form the base of the download file name.
-#'
-#' @return A `downloadHandler` object for use in the server function of a
-#' Shiny app. This handler will allow the user to download a file
-#' with the desired format and name.
-#'
-#' @details
-#' The filename is generated based on the table name prefix and current date,
-#' with the appropriate file extension determined by the user's selection.
-#' Supported extensions are: `.xlsx`, `.csv`, `.png`, and `.html`. The content
-#' of the file is copied from `export_file`, and a notification is shown to
-#' indicate that the file is being generated.
-#'
-#' @examples
-#' \dontrun{
-#' # Create a download handler for a CSV file
-#' create_download_handler("path/to/file.csv", reactive("csv"), reactive("data-table"))
-#' }
-#'
-create_download_handler <- function(local) {
- downloadHandler(
- filename = function() {
- file_ext <- dplyr::case_when(
- grepl("xlsx", local$file_type, ignore.case = TRUE) ~ ".xlsx",
- grepl("csv", local$file_type, ignore.case = TRUE) ~ ".csv",
- grepl("svg", local$file_type, ignore.case = TRUE) ~ ".svg",
- grepl("html", local$file_type, ignore.case = TRUE) ~ ".html",
- TRUE ~ "Error"
- )
- paste0(paste(local$file_name, collapse = "-"), "-", Sys.Date(), file_ext)
- },
- content = function(file) {
- pop_up <- shiny::showNotification("Generating download file", duration = NULL)
- file.copy(local$export_file, file)
- on.exit(shiny::removeNotification(pop_up), add = TRUE)
- }
- )
-}
-
-
-#' Generate a Radio Button Input for File Type Selection
-#'
-#' This function creates a radio button input for selecting the download file
-#' format in a Shiny application. The label, hint, and choices for the radio
-#' button are dynamically generated based on the type of file being downloaded
-#' (either a data table or plot).
-#'
-#' @param input_id A string representing the input ID for the radio button,
-#' which will be used to access the selected file type in the Shiny
-#' server logic.
-#' @param file_type A string that specifies the type of file being downloaded.
-#' It can either be "table" (for downloading data tables) or any other
-#' string (for downloading plots). Defaults to "table".
-#'
-#' @return A `shinyGovstyle::radio_button_Input` object to be included in the
-#' Shiny UI, allowing the user to choose between available file formats
-#' for the download.
-#'
-#' @details
-#' When the `file_type` is "table", the user will have the option to select
-#' between "CSV" and "XLSX" file formats. For other file types, the user can
-#' select between "PNG" and "HTML". The default selected option is "CSV" for
-#' tables and "PNG" for plots. The hint label displayed below the input will
-#' provide guidance based on the type of download.
-#'
-#' @examples
-#' \dontrun{
-#' # Generate file type selection for a table
-#' file_type_input_btn("file_type", file_type = "table")
-#'
-#' # Generate file type selection for a plot
-#' file_type_input_btn("file_type", file_type = "plot")
-#' }
-#'
-file_type_input_btn <- function(input_id, file_type = "table") {
- shinyGovstyle::radio_button_Input(
- inputId = input_id,
- label = h2("Choose download file format"),
- hint_label = if (file_type == "table") {
- paste0(
- "This will download all data related to the providers and options selected.",
- " The XLSX format is designed for use in Microsoft Excel."
- )
- } else {
- paste0(
- "This will download the plots related to the options selected.",
- " The HTML format contains the interactive element."
- )
- },
- choices = if (file_type == "table") {
- c("CSV", "XLSX")
- } else {
- c("SVG", "HTML")
- },
- selected = if (file_type == "table") "CSV" else "SVG"
- )
-}
-
-
-#' Update and fetch metadata for a given indicator
-#'
-#' This function retrieves the metadata for a specified indicator and updates
-#' the associated reactive storage. If the indicator is empty, the previously
-#' stored value is returned.
-#'
-#' @param input_indicator A string representing the selected indicator. If
-#' empty, the function returns the previously stored value.
-#' @param metadata_type A string specifying the type of metadata to fetch (e.g.,
-#' "Description", "Methodology").
-#' @param reactive_storage A `reactiveValues` object where the metadata is
-#' stored and updated.
-#' @param key A string representing the key in `reactive_storage` corresponding
-#' to the metadata type.
-#'
-#' @return The metadata associated with the specified indicator and metadata
-#' type. If the indicator is empty, the previously stored value is returned.
-#'
-#' @examples
-#' \dontrun{
-#' previous_metadata <- reactiveValues(description = NULL)
-#' update_and_fetch_metadata(
-#' input_indicator = "Indicator A",
-#' metadata_type = "Description",
-#' reactive_storage = previous_metadata,
-#' key = "description"
-#' )
-#' }
-#'
-update_and_fetch_metadata <- function(input_indicator,
- metadata_type,
- reactive_storage,
- key) {
- if (input_indicator == "") {
- return(reactive_storage[[key]])
- }
-
- # Fetch the metadata for the selected indicator
- metadata <- metrics_clean |>
- get_metadata(input_indicator, metadata_type)
-
- # Update the previous value in the reactive storage
- reactive_storage[[key]] <- metadata
-
- return(metadata)
-}
-
-
-
-read_data_dict_shared_folder <- function(shared_folder, sheet_name) {
- readxl::read_xlsx(
- path = paste0(shared_folder, "/../Information for App Development/LAIT Data Dictionary (To QA!).xlsx"),
- sheet = sheet_name,
- # Replace multi-space with single-space
- .name_repair = clean_spaces
- )
-}
+#' Shared Folder Path
+#'
+#' This variable holds the path to the 'LAIT - modernisation' teams channel folder.
+#' The folder is synchronised and located in the user's local system.
+#'
+shared_folder <- paste0(
+ r"(C:\Users\jtufts\Department for Education\LAIT modernisation - General)",
+ r"(\LAIT Modernisation 2024\Information for App Development)"
+)
+
+
+#' Clean SNP Column Names
+#'
+#' This function cleans the column names of a given data frame. Specifically, it adds numbers to 'SNP' columns
+#' based on the numbers extracted from 'SN' columns.
+#'
+#' @param data A data frame that contains columns with names starting with 'SN' and 'SNP'.
+#'
+#' @return A data frame with cleaned column names.
+#'
+#' @examples
+#' \dontrun{
+#' data <- data.frame(SN1 = c(1, 2), SNP = c(3, 4), SN2 = c(5, 6), SNP = c(7, 8))
+#' clean_snp_colnames(data)
+#' }
+#'
+#' @export
+clean_snp_colnames <- function(data) {
+ col_names <- colnames(data)
+
+ # Logical vectors to identify "SN" and "SNP" columns
+ sn_cols <- which(grepl("^SN\\d+$", col_names))
+ snp_cols <- which(grepl("^SNP", col_names))
+
+ if (length(sn_cols) < 1) {
+ stop(
+ paste0(
+ "SN columns do not seem to be in the right format e.g., SNx",
+ "where x is a number"
+ )
+ )
+ }
+
+ if (length(snp_cols) < 1) {
+ stop(
+ paste0(
+ "SNP columns do not seem to be in the right format e.g., SNPx",
+ "where x can be anything"
+ )
+ )
+ }
+
+ # Extract the numbers from "SN" columns
+ sn_numbers <- gsub("^SN", "", col_names[sn_cols])
+
+ # Clean column names vector
+ clean_col_names <- col_names
+
+ # Assign new names to "SNP" columns using extracted "SN" column numbers
+ clean_col_names[snp_cols] <- paste0("SNP", sn_numbers)
+
+ # Assign the new column names to the dataframe
+ colnames(data) <- clean_col_names
+
+ data
+}
+
+
+#' Create Measure Key
+#'
+#' This function creates a new column 'measure_key' in the given data frame. The 'measure_key' is created by
+#' concatenating the 'Topic' and 'Measure_short' columns, replacing spaces with underscores, and converting
+#' the resulting string to lowercase.
+#'
+#' @param data A data frame that contains the columns 'Topic' and 'Measure_short'.
+#'
+#' @return A data frame with an additional 'measure_key' column.
+#'
+#' @examples
+#' \dontrun{
+#' data <- data.frame(Topic = c("Topic1", "Topic2"), Measure_short = c("Measure1", "Measure2"))
+#' create_measure_key(data)
+#' }
+#'
+#' @export
+create_measure_key <- function(data) {
+ data |>
+ dplyr::mutate(
+ measure_key = tolower(gsub(" ", "_", paste(Topic, Measure_short))),
+ .after = Measure_short
+ )
+}
+
+
+#' Generate Downloadable File Based on Data and File Type
+#'
+#' This function generates a temporary file for download based on the provided
+#' data and file type. Supported file types include CSV, XLSX, PNG, and HTML.
+#' The function writes the data to the corresponding file format, and returns
+#' the file path for download.
+#'
+#' @param data A dataset or object to be saved, which can be a data frame or
+#' a reactive list containing specific data for charts or widgets.
+#' @param file_type A string specifying the desired file type for download.
+#' Supported types are: "csv", "xlsx", "png", and "html". This string
+#' is matched case-insensitively.
+#'
+#' @return A character string representing the path to the generated file.
+#' The file will be saved temporarily with the appropriate extension
+#' based on the provided `file_type`.
+#'
+#' @details
+#' The function handles multiple file types as follows:
+#' - For `"csv"`, it writes the data to a CSV file without row names.
+#' - For `"xlsx"`, it saves the data as an Excel file with auto column widths.
+#' - For `"png"`, it saves a ggplot object to a PNG file with specified size.
+#' - For `"html"`, it saves an HTML widget to an HTML file.
+#' If an unsupported file type is provided, the function returns an error.
+#'
+#' @examples
+#' \dontrun{
+#' # Save a data frame as a CSV file
+#' generate_download_file(mtcars, "csv")
+#'
+#' # Save a ggplot chart as PNG
+#' plot_data <- list(png = ggplot(mtcars, aes(mpg, wt)) +
+#' geom_point())
+#' generate_download_file(plot_data, "png")
+#' }
+#'
+generate_download_file <- function(data, file_type, svg_width = 8.5) {
+ out <- tempfile(fileext = dplyr::case_when(
+ grepl("csv", file_type, ignore.case = TRUE) ~ ".csv",
+ grepl("xlsx", file_type, ignore.case = TRUE) ~ ".xlsx",
+ grepl("svg", file_type, ignore.case = TRUE) ~ ".svg",
+ grepl("html", file_type, ignore.case = TRUE) ~ ".html",
+ TRUE ~ "Error"
+ ))
+
+ if (grepl("csv", file_type, ignore.case = TRUE)) {
+ write.csv(data, file = out, row.names = FALSE)
+ } else if (grepl("xlsx", file_type, ignore.case = TRUE)) {
+ openxlsx::write.xlsx(data, file = out, colWidths = "Auto")
+ } else if (grepl("svg", file_type, ignore.case = TRUE)) {
+ ggplot2::ggsave(filename = out, plot = data, width = svg_width, height = 6)
+ } else if (grepl("html", file_type, ignore.case = TRUE)) {
+ htmlwidgets::saveWidget(widget = data, file = out)
+ }
+
+ out
+}
+
+
+#' Create Download Handler for Shiny Application
+#'
+#' This function creates a `downloadHandler` for use in a Shiny app, allowing
+#' users to download a file in the specified format. The file name is generated
+#' dynamically based on the provided extension input and table name prefix.
+#'
+#' @param export_file A string representing the file path of the file to be
+#' downloaded. This should be a pre-generated file ready for download.
+#' @param ext_input A reactive expression that returns the file type selected
+#' by the user. Supported types are: "xlsx", "csv", "png", and "html".
+#' The file extension is matched case-insensitively.
+#' @param table_name_prefix A reactive expression returning a vector of strings
+#' that represent parts of the file name. These are concatenated with
+#' hyphens (`-`) to form the base of the download file name.
+#'
+#' @return A `downloadHandler` object for use in the server function of a
+#' Shiny app. This handler will allow the user to download a file
+#' with the desired format and name.
+#'
+#' @details
+#' The filename is generated based on the table name prefix and current date,
+#' with the appropriate file extension determined by the user's selection.
+#' Supported extensions are: `.xlsx`, `.csv`, `.png`, and `.html`. The content
+#' of the file is copied from `export_file`, and a notification is shown to
+#' indicate that the file is being generated.
+#'
+#' @examples
+#' \dontrun{
+#' # Create a download handler for a CSV file
+#' create_download_handler("path/to/file.csv", reactive("csv"), reactive("data-table"))
+#' }
+#'
+create_download_handler <- function(local) {
+ downloadHandler(
+ filename = function() {
+ file_ext <- dplyr::case_when(
+ grepl("xlsx", local$file_type, ignore.case = TRUE) ~ ".xlsx",
+ grepl("csv", local$file_type, ignore.case = TRUE) ~ ".csv",
+ grepl("svg", local$file_type, ignore.case = TRUE) ~ ".svg",
+ grepl("html", local$file_type, ignore.case = TRUE) ~ ".html",
+ TRUE ~ "Error"
+ )
+ paste0(paste(local$file_name, collapse = "-"), "-", Sys.Date(), file_ext)
+ },
+ content = function(file) {
+ pop_up <- shiny::showNotification("Generating download file", duration = NULL)
+ file.copy(local$export_file, file)
+ on.exit(shiny::removeNotification(pop_up), add = TRUE)
+ }
+ )
+}
+
+
+# Helper function to calculate actual file size
+calculate_file_size <- function(file_type, data) {
+ # Create a temporary file
+ temp_file <- tempfile(fileext = paste0(".", tolower(file_type)))
+
+ # Create file or return estimated size
+ if (file_type == "CSV") {
+ write.csv(data, temp_file, row.names = FALSE)
+ } else if (file_type == "XLSX") {
+ openxlsx::write.xlsx(data, temp_file, colWidths = "auto")
+ } else if (file_type == "SVG") {
+ return("usually 20 KB and no larger than 200 KB")
+ } else if (file_type == "HTML") {
+ return("usually 275 KB and no larger than 500 KB")
+ }
+
+ # Get the file size in KB
+ file_size_kb <- ceiling((file.size(temp_file) / 1024) / 5) * 5
+
+ # Round file size to nearest 10, while handling small sizes correctly
+ rounded_file_size <- round(file_size_kb, 2)
+
+ unlink(temp_file) # Remove the temporary file
+ return(paste0(rounded_file_size, " KB"))
+}
+
+
+#' Generate a Radio Button Input for File Type Selection
+#'
+#' This function creates a radio button input for selecting the download file
+#' format in a Shiny application. The label, hint, and choices for the radio
+#' button are dynamically generated based on the type of file being downloaded
+#' (either a data table or plot).
+#'
+#' @param input_id A string representing the input ID for the radio button,
+#' which will be used to access the selected file type in the Shiny
+#' server logic.
+#' @param file_type A string that specifies the type of file being downloaded.
+#' It can either be "table" (for downloading data tables) or any other
+#' string (for downloading plots). Defaults to "table".
+#'
+#' @return A `shinyGovstyle::radio_button_Input` object to be included in the
+#' Shiny UI, allowing the user to choose between available file formats
+#' for the download.
+#'
+#' @details
+#' When the `file_type` is "table", the user will have the option to select
+#' between "CSV" and "XLSX" file formats. For other file types, the user can
+#' select between "PNG" and "HTML". The default selected option is "CSV" for
+#' tables and "PNG" for plots. The hint label displayed below the input will
+#' provide guidance based on the type of download.
+#'
+#' @examples
+#' \dontrun{
+#' # Generate file type selection for a table
+#' file_type_input_btn("file_type", file_type = "table")
+#'
+#' # Generate file type selection for a plot
+#' file_type_input_btn("file_type", file_type = "plot")
+#' }
+#'
+file_type_input_btn <- function(input_id, data = NULL, file_type = "table") {
+ # Generate choices with actual file size
+ choices_with_size <- if (file_type == "table") {
+ c(
+ paste0("CSV (less than ", calculate_file_size("CSV", data), ")"),
+ paste0("XLSX (less than ", calculate_file_size("XLSX", data), ")")
+ )
+ } else {
+ c(
+ paste0("SVG (", calculate_file_size("SVG", data), ")"),
+ paste0("HTML (", calculate_file_size("HTML", data), ")")
+ )
+ }
+
+ shinyGovstyle::radio_button_Input(
+ inputId = input_id,
+ label = h2("Choose download file format"),
+ hint_label = if (file_type == "table") {
+ paste0(
+ "This will download all data related to the providers and options selected.",
+ " The XLSX format is designed for use in Microsoft Excel."
+ )
+ } else {
+ paste0(
+ "This will download the plots related to the options selected.",
+ " The HTML format contains the interactive element."
+ )
+ },
+ choices = choices_with_size,
+ selected = choices_with_size[1]
+ )
+}
+
+
+#' Update and fetch metadata for a given indicator
+#'
+#' This function retrieves the metadata for a specified indicator and updates
+#' the associated reactive storage. If the indicator is empty, the previously
+#' stored value is returned.
+#'
+#' @param input_indicator A string representing the selected indicator. If
+#' empty, the function returns the previously stored value.
+#' @param metadata_type A string specifying the type of metadata to fetch (e.g.,
+#' "Description", "Methodology").
+#' @param reactive_storage A `reactiveValues` object where the metadata is
+#' stored and updated.
+#' @param key A string representing the key in `reactive_storage` corresponding
+#' to the metadata type.
+#'
+#' @return The metadata associated with the specified indicator and metadata
+#' type. If the indicator is empty, the previously stored value is returned.
+#'
+#' @examples
+#' \dontrun{
+#' previous_metadata <- reactiveValues(description = NULL)
+#' update_and_fetch_metadata(
+#' input_indicator = "Indicator A",
+#' metadata_type = "Description",
+#' reactive_storage = previous_metadata,
+#' key = "description"
+#' )
+#' }
+#'
+update_and_fetch_metadata <- function(input_indicator,
+ metadata_type,
+ reactive_storage,
+ key) {
+ if (input_indicator == "") {
+ return(reactive_storage[[key]])
+ }
+
+ # Fetch the metadata for the selected indicator
+ metadata <- metrics_clean |>
+ get_metadata(input_indicator, metadata_type)
+
+ # Update the previous value in the reactive storage
+ reactive_storage[[key]] <- metadata
+
+ return(metadata)
+}
+
+
+
+read_data_dict_shared_folder <- function(shared_folder, sheet_name) {
+ readxl::read_xlsx(
+ path = paste0(shared_folder, "/../Information for App Development/LAIT Data Dictionary (To QA!).xlsx"),
+ sheet = sheet_name,
+ # Replace multi-space with single-space
+ .name_repair = clean_spaces
+ )
+}
diff --git a/R/fn_table_helpers.R b/R/fn_table_helpers.R
index 9c0aa3a8..c0056e8d 100644
--- a/R/fn_table_helpers.R
+++ b/R/fn_table_helpers.R
@@ -645,8 +645,8 @@ build_sn_stats_table <- function(
#' }
#' )
#' }
-highlight_selected_row <- function(index, data, selected_area = NULL) {
- la_region <- data[index, "LA and Regions"]
+highlight_selected_row <- function(index, data, selected_area = NULL, geog_col = "LA and Regions") {
+ la_region <- data[index, geog_col]
# Handle missing values first
if (is.na(la_region)) {
diff --git a/R/fn_ui_layout.R b/R/fn_ui_layout.R
index aab35679..edcddcd3 100644
--- a/R/fn_ui_layout.R
+++ b/R/fn_ui_layout.R
@@ -37,7 +37,7 @@ create_download_options_ui <- function(download_id, copy_clipboard_id) {
actionButton(
copy_clipboard_id,
"Copy Chart to Clipboard",
- icon = icon("copy"),
+ icon = icon("copy", `aria-hidden` = "true"),
class = "gov-uk-button"
),
style = "max-width: none;"
@@ -66,7 +66,8 @@ create_hidden_clipboard_plot <- function(clipboard_plot_id) {
# Hidden static plot for copy-to-clipboard
div(
shiny::plotOutput(clipboard_plot_id),
- style = "content-visibility: hidden;"
+ style = "content-visibility: hidden;",
+ `aria-hidden` = "true"
)
}
@@ -157,8 +158,8 @@ full_data_on_github_noti <- function() {
input_id = "full_data_on_github",
title_txt = "Information",
body_txt = shiny::HTML(paste0(
- "The full dataset is available to download as a .csv on GitHub. ",
- "The file is ",
+ "The full dataset is available for download as a CSV on GitHub. ",
+ "You can access the file ",
dfeshiny::external_link(
href = paste0(
"https://github.com/dfe-analytical-services/",
@@ -168,9 +169,201 @@ full_data_on_github_noti <- function() {
),
".
",
"",
- "This should be the preferred method for large data downloads ",
- "especially for use with code."
+ "We recommend this method for downloading large datasets, ",
+ "especially if you plan to use the data in your code."
)),
type = "standard"
)
}
+
+
+dfe_footer <- function(links_list) {
+ # Add the HTML around the link and make an id by snake casing
+ create_footer_link <- function(link_text) {
+ shiny::tags$li(
+ class = "govuk-footer__inline-list-item",
+ actionLink(
+ class = "govuk-link govuk-footer__link",
+ inputId = tolower(gsub(" ", "_", link_text)),
+ label = link_text
+ )
+ )
+ }
+
+ # The HTML div to be returned
+ shiny::tags$footer(
+ class = "govuk-footer ",
+ role = "contentinfo",
+ shiny::div(class = "govuk-width-container ", shiny::div(
+ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ # Add custom links in
+ shiny::div(
+ class = "govuk-footer__meta-item govuk-footer__meta-item--grow",
+
+ # Set a visually hidden title for accessibility
+ shiny::h2(class = "govuk-visually-hidden", "Support links"),
+ # Generate as many links as needed
+ shiny::tags$ul(
+ class = "govuk-footer__inline-list",
+ lapply(links_list, create_footer_link)
+ )
+ ),
+
+ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ # Back to copied code from shinyGovstyle
+ shiny::div(class = "govuk-footer__meta", shiny::tagList(
+ shiny::div(
+ class = "govuk-footer__meta-item govuk-footer__meta-item--grow",
+ shiny::tag(
+ "svg",
+ list(
+ role = "presentation",
+ focusable = "false",
+ class = "govuk-footer__licence-logo",
+ xmlns = "http://www.w3.org/2000/svg",
+ viewbox = "0 0 483.2 195.7",
+ height = "17",
+ width = "41",
+ shiny::tag("path", list(
+ fill = "currentColor",
+ d = paste0(
+ "M421.5 142.8V.1l-50.7 32.3v161.1h112.4v-50.7",
+ "zm-122.3-9.6A47.12 47.12 0 0 1 221 97.8c0-26 21",
+ ".1-47.1 47.1-47.1 16.7 0 31.4 8.7 39.7 21.8l42.7",
+ "-27.2A97.63 97.63 0 0 0 268.1 0c-36.5 0-68.3 20.1",
+ "-85.1 49.7A98 98 0 0 0 97.8 0C43.9 0 0 43.9 0 97",
+ ".8s43.9 97.8 97.8 97.8c36.5 0 68.3-20.1 85.1-49.",
+ "7a97.76 97.76 0 0 0 149.6 25.4l19.4 22.2h3v-87.8",
+ "h-80l24.3 27.5zM97.8 145c-26 0-47.1-21.1-47.1-47",
+ ".1s21.1-47.1 47.1-47.1 47.2 21 47.2 47S123.8 145",
+ " 97.8 145"
+ )
+ ))
+ )
+ ),
+ shiny::tags$span(
+ class = "govuk-footer__licence-description",
+ "All content is available under the",
+ shiny::tags$a(
+ class = "govuk-footer__link",
+ href = "https://www.nationalarchives.gov.uk/doc/open-government-licence/version/3/",
+ rel = "license",
+ "Open Government Licence v3.0",
+ .noWS = "after"
+ ),
+ ", except where otherwise stated"
+ )
+ ),
+ shiny::tags$div(
+ class = "govuk-footer__meta-item",
+ shiny::tags$a(
+ class = "govuk-footer__link govuk-footer__copyright-logo",
+ href =
+ paste0(
+ "https://www.nationalarchives.gov.uk/information-management/",
+ "re-using-public-sector-information/uk-government-licensing-framework/crown-copyright/"
+ ),
+ "\u00A9 Crown copyright"
+ )
+ )
+ ))
+ ))
+ )
+}
+
+
+# left nav ====================================================================
+dfe_contents_links <- function(links_list) {
+ # Add the HTML around the link and make an id by snake casing
+ create_sidelink <- function(link_text) {
+ tags$li(
+ "—",
+ actionLink(tolower(gsub(
+ " ", "_", link_text
+ )), link_text, class = "contents_link")
+ )
+ }
+
+ # The HTML div to be returned
+ tags$div(
+ style = "position: sticky; top: 0.5rem; padding: 0.25rem; word-break: break-word;",
+ # Make it stick!
+ h2("Contents"),
+ # remove the circle bullets
+ tags$ol(
+ style = "list-style-type: none; padding-left: 0; font-size: 1rem;",
+ lapply(links_list, create_sidelink)
+ )
+ )
+}
+
+
+cookies_banner_server_jt <- function(id = "cookies_banner",
+ input_cookies,
+ parent_session,
+ google_analytics_key = NULL,
+ cookies_link_panel = "cookies_panel_ui") {
+ shiny::moduleServer(id, function(input, output, session) {
+ if (is.null(google_analytics_key)) {
+ warning("Please provide a valid Google Analytics key")
+ }
+ shiny::observeEvent(input_cookies(), {
+ if (!is.null(input_cookies())) {
+ if (!("dfe_analytics" %in% names(input_cookies()))) {
+ shinyjs::show(id = "cookies_main")
+ } else {
+ shinyjs::hide(id = "cookies_main")
+ msg <- list(
+ name = "dfe_analytics",
+ value = input_cookies()$dfe_analytics
+ )
+ session$sendCustomMessage("analytics-consent", msg)
+ if ("cookies" %in% names(input)) {
+ if ("dfe_analytics" %in% names(input_cookies())) {
+ if (input_cookies()$dfe_analytics == "denied") {
+ ga_msg <- list(name = paste0("_ga_", google_analytics_key))
+ session$sendCustomMessage("cookie-clear", ga_msg)
+ }
+ }
+ }
+ }
+ } else {
+ shinyjs::hide(id = "cookies_main", asis = TRUE)
+ shinyjs::toggle(id = "cookies_div", asis = TRUE)
+ }
+ })
+ shiny::observeEvent(input$cookies_accept, {
+ msg <- list(name = "dfe_analytics", value = "granted")
+ session$sendCustomMessage("cookie-set", msg)
+ session$sendCustomMessage("analytics-consent", msg)
+ shinyjs::hide(id = "cookies_main", asis = TRUE)
+ })
+ shiny::observeEvent(input$cookies_reject, {
+ msg <- list(name = "dfe_analytics", value = "denied")
+ session$sendCustomMessage("cookie-set", msg)
+ session$sendCustomMessage("analytics-consent", msg)
+ shinyjs::hide(id = "cookies_main", asis = TRUE)
+ })
+ shiny::observeEvent(input$cookies_link, {
+ shiny::updateTabsetPanel(
+ session = parent_session, "pages",
+ selected = cookies_link_panel
+ )
+ })
+ return(shiny::renderText({
+ cookies_text_stem <- "You have chosen to"
+ cookies_text_tail <- "the use of cookies on this website."
+ if (!is.null(input_cookies())) {
+ if ("dfe_analytics" %in% names(input_cookies())) {
+ if (input_cookies()$dfe_analytics == "granted") {
+ paste(cookies_text_stem, "accept", cookies_text_tail)
+ } else {
+ paste(cookies_text_stem, "reject", cookies_text_tail)
+ }
+ }
+ } else {
+ "Cookies consent has not been confirmed."
+ }
+ }))
+ })
+}
diff --git a/R/lait_modules/mod_all_la_table.R b/R/lait_modules/mod_all_la_table.R
index 5925ddc2..e4406e2f 100644
--- a/R/lait_modules/mod_all_la_table.R
+++ b/R/lait_modules/mod_all_la_table.R
@@ -152,34 +152,32 @@ AllLA_TableUI <- function(id) {
div(
class = "well",
style = "overflow-y: visible;",
- bslib::navset_tab(
+ bslib::navset_card_tab(
id = "all_la_table_tabs",
bslib::nav_panel(
"Tables",
- bslib::card(
- bslib::card_header(
- Get_AllLATableNameUI(ns("table_name")),
- style = "text-align: center;"
- ),
- bslib::card_header("Local Authorities"),
+ bslib::card_header(
+ Get_AllLATableNameUI(ns("table_name")),
+ style = "text-align: center;"
+ ),
+ bslib::card_header("Local Authorities"),
+ with_gov_spinner(
+ reactable::reactableOutput(ns("la_table")),
+ size = 3
+ ),
+ div(
+ # Add black border between the tables
+ style = "border-top: 2px solid black; padding-top: 2.5rem;",
+ bslib::card_header("Regions"),
with_gov_spinner(
- reactable::reactableOutput(ns("la_table")),
- size = 3
- ),
- div(
- # Add black border between the tables
- style = "border-top: 2px solid black; padding-top: 2.5rem;",
- bslib::card_header("Regions"),
- with_gov_spinner(
- reactable::reactableOutput(ns("region_table")),
- size = 1.6
- )
+ reactable::reactableOutput(ns("region_table")),
+ size = 1.6
)
)
),
bslib::nav_panel(
"Download data",
- file_type_input_btn(ns("file_type")),
+ shiny::uiOutput(ns("download_file_txt")),
Download_DataUI(ns("all_download"), "All Geographies Table"),
Download_DataUI(ns("la_download"), "LA Table"),
Download_DataUI(ns("region_download"), "Region Table")
@@ -235,6 +233,13 @@ AllLA_TableServer <- function(id, app_inputs, bds_metrics, la_names_bds) {
)
# All geographies table download ------------------------------------------
+ # File download text - calculates file size
+ ns <- NS(id)
+ output$download_file_txt <- shiny::renderUI({
+ file_type_input_btn(ns("file_type"), all_la_table())
+ })
+
+ # Download dataset
Download_DataServer(
"all_download",
reactive(input$file_type),
@@ -262,7 +267,7 @@ AllLA_TableServer <- function(id, app_inputs, bds_metrics, la_names_bds) {
set_custom_default_col_widths()
),
rowStyle = function(index) {
- highlight_selected_row(index, all_la_la_table, app_inputs$la())
+ highlight_selected_row(index, all_la_la_table, app_inputs$la(), "LA")
},
pagination = FALSE
)
@@ -287,10 +292,10 @@ AllLA_TableServer <- function(id, app_inputs, bds_metrics, la_names_bds) {
# Sums number of non-NA cols (left of LA and Regions) and checks if = 0
rowSums(!is.na(dplyr::select(all_la_table(), -c(`LA Number`, `LA and Regions`)))) == 0)
) |>
- # Replace Rank with a blank col
+ # Replace Rank
dplyr::mutate(Rank = "") |>
- dplyr::rename(` ` = "Rank") |>
- dplyr::arrange(`LA Number`)
+ dplyr::arrange(`LA Number`) |>
+ dplyr::rename("Region" = `LA and Regions`)
# Get region of LA
all_la_region <- stat_n_la |>
@@ -309,10 +314,19 @@ AllLA_TableServer <- function(id, app_inputs, bds_metrics, la_names_bds) {
num_exclude = "LA Number",
categorical = "Rank"
),
- set_custom_default_col_widths()
+ list(
+ set_custom_default_col_widths(),
+ Rank = reactable::colDef(
+ header = add_tooltip_to_reactcol(
+ "Rank",
+ "Regions are not currently ranked",
+ placement = "top"
+ )
+ )
+ )
),
rowStyle = function(index) {
- highlight_selected_row(index, all_la_region_table, all_la_region)
+ highlight_selected_row(index, all_la_region_table, all_la_region, "Region")
},
pagination = FALSE
# class = "hidden-column-headers"
diff --git a/R/lait_modules/mod_app_helpers.R b/R/lait_modules/mod_app_helpers.R
index 43f898d4..e412f62d 100644
--- a/R/lait_modules/mod_app_helpers.R
+++ b/R/lait_modules/mod_app_helpers.R
@@ -19,7 +19,7 @@ PageHeaderUI <- function(id) {
shinycssloaders::withSpinner(
shiny::uiOutput(ns("page_header")),
type = 7,
- color = "#1d70b8",
+ color = "#0b0c0c",
size = 1,
proxy.height = paste0(250 * 0.25, "px")
)
@@ -238,7 +238,8 @@ DownloadChartModalUI <- function(id, chart_type) {
footer = shiny::tagAppendAttributes(
shiny::modalButton("Close"),
class = "govuk-button--secondary"
- )
+ ),
+ size = "l"
)
}
diff --git a/R/lait_modules/mod_app_inputs.R b/R/lait_modules/mod_app_inputs.R
index 1fd03c96..edc9a718 100644
--- a/R/lait_modules/mod_app_inputs.R
+++ b/R/lait_modules/mod_app_inputs.R
@@ -1,182 +1,182 @@
-# nolint start: object_name
-#
-#' Shiny Module UI for Displaying the App Inputs
-#'
-#' This function creates a Shiny UI module for displaying the app inputs.
-#' The inputs include a select input for the local authority (LA) name,
-#' the topic name, and the indicator name.
-#' Each input is wrapped in a div with a well and a layout column for styling.
-#'
-#' @param id A unique ID that identifies the UI element
-#' @return A div object that contains the UI elements for the module
-#'
-appInputsUI <- function(id) {
- ns <- NS(id)
-
- div(
- class = "well",
- style = "overflow-y: visible; position: relative;",
- bslib::layout_column_wrap(
- width = "15rem", # Minimum width for each input box before wrapping
- shiny::selectizeInput(
- inputId = ns("la_name"),
- label = tags$label(
- "Local Authority:",
- create_tooltip_icon("Change selection by scrolling or typing")
- ),
- choices = la_names_bds,
- options = list(
- placeholder = "Start typing or scroll to find a Local Authority...",
- plugins = list("clear_button")
- )
- ),
- shiny::selectizeInput(
- inputId = ns("topic_name"),
- label = tags$label(
- id = ns("topic_label"),
- "Topic:"
- ),
- choices = c("All Topics", metric_topics),
- selected = "All Topics",
- options = list(
- placeholder = "No topic selected, showing all indicators...",
- plugins = list("clear_button")
- )
- ),
- shiny::selectizeInput(
- inputId = ns("indicator_name"),
- label = "Indicator:",
- choices = metric_names,
- options = list(
- placeholder = "Start typing or scroll to find an indicator...",
- plugins = list("clear_button")
- )
- )
- )
- )
-}
-
-
-#' Shiny Server Function for Handling the App Inputs with Synchronisation
-#'
-#' This function creates a Shiny server module for handling the app inputs
-#' and synchronising them across multiple pages.
-#' It observes the selected topic name and updates the choices for the
-#' indicator name based on the selected topic, and also updates the shared
-#' reactive values to keep the inputs in sync between pages.
-#'
-#' @param id A unique ID that identifies the server function.
-#' @param shared_values A `reactiveValues` object to store shared input values
-#' that can be accessed and modified across different modules.
-#' @return A list of reactive expressions for the app inputs, including
-#' the selected LA name, topic name, and indicator name.
-#'
-appInputsServer <- function(id,
- shared_values,
- topic_indicator_full) {
- moduleServer(id, function(input, output, session) {
- # Reactive value to store the previous LA name
- previous_la_name <- reactiveVal(NULL)
-
- # Debounce input values to prevent looping when inputs change quickly
- debounced_la_name <- shiny::debounce(reactive(input$la_name), 150)
- debounced_topic_name <- shiny::debounce(reactive(input$topic_name), 150)
- debounced_indicator_name <- shiny::debounce(reactive(input$indicator_name), 150)
-
- # Update Indicator dropdown for selected Topic
- shiny::observeEvent(debounced_topic_name(),
- {
- # Save the currently selected indicator
- current_indicator <- debounced_indicator_name()
-
- # Determine the filter condition for Topic
- topic_filter <- debounced_topic_name()
-
- # Get indicator choices for selected topic
- # Include all rows if no topic is selected or "All Topics" is selected
- filtered_topic_bds <- topic_indicator_full |>
- filter_by_topic("Topic", topic_filter) |>
- pull_uniques("Measure")
-
- # Ensure the current indicator stays selected
- # Default to the first topic indicator if the current is not valid
- selected_indicator <- if (current_indicator %in% filtered_topic_bds) {
- current_indicator
- } else {
- filtered_topic_bds[1]
- }
-
- # Update the Indicator dropdown based on selected Topic
- shiny::updateSelectizeInput(
- session = session,
- inputId = "indicator_name",
- choices = filtered_topic_bds,
- selected = selected_indicator
- )
-
- # Update the shared reactive value for the topic
- shared_values$topic <- debounced_topic_name()
- },
- ignoreNULL = FALSE
- )
-
- # Prevent LA input from being empty by storing its previous value
- shiny::observeEvent(debounced_la_name(), {
- # Check if the LA name is NULL or empty
- la_name <- debounced_la_name()
-
- if ("" %notin% la_name && !is.null(la_name)) {
- # Update the reactive value with the current valid input
- previous_la_name(la_name)
-
- # Synchronise the shared reactive value
- shared_values$la <- la_name
- }
- })
-
- # Set dynamic topic label
- # (to display topic when not selected or all topics selected)
- update_topic_label(
- indicator_input = debounced_indicator_name,
- topic_input = debounced_topic_name,
- topic_indicator_data = topic_indicator_full,
- topic_label_id = "topic_label"
- )
-
- # Observe and synchronise Indicator input changes
- observeEvent(debounced_indicator_name(), {
- shared_values$indicator <- debounced_indicator_name()
- })
-
- # Synchronise inputs across pages:
- # LA
- observe({
- shiny::updateSelectizeInput(session, "la_name", selected = shared_values$la)
- })
- # Topic
- observe({
- shiny::updateSelectizeInput(session, "topic_name", selected = shared_values$topic)
- })
- # Indicator
- observe({
- shiny::updateSelectizeInput(session, "indicator_name", selected = shared_values$indicator)
- })
-
- # Return reactive settings
- app_settings <- list(
- la = reactive({
- previous_la_name()
- }),
- topic = reactive({
- debounced_topic_name()
- }),
- indicator = reactive({
- debounced_indicator_name()
- })
- )
-
- return(app_settings)
- })
-}
-
-# nolint end
+# nolint start: object_name
+#
+#' Shiny Module UI for Displaying the App Inputs
+#'
+#' This function creates a Shiny UI module for displaying the app inputs.
+#' The inputs include a select input for the local authority (LA) name,
+#' the topic name, and the indicator name.
+#' Each input is wrapped in a div with a well and a layout column for styling.
+#'
+#' @param id A unique ID that identifies the UI element
+#' @return A div object that contains the UI elements for the module
+#'
+appInputsUI <- function(id) {
+ ns <- NS(id)
+
+ div(
+ class = "well",
+ style = "overflow-y: visible; position: relative;",
+ bslib::layout_column_wrap(
+ width = "15rem", # Minimum width for each input box before wrapping
+ shiny::selectizeInput(
+ inputId = ns("la_name"),
+ label = tags$label(
+ "Local Authority:",
+ create_tooltip_icon("Change selection by scrolling or typing")
+ ),
+ choices = la_names_bds,
+ options = list(
+ placeholder = "Start typing or scroll to find a Local Authority...",
+ plugins = list("clear_button")
+ )
+ ),
+ shiny::selectizeInput(
+ inputId = ns("topic_name"),
+ label = tags$label(
+ id = ns("topic_label"),
+ "Topic:"
+ ),
+ choices = c("All Topics", metric_topics),
+ selected = "All Topics",
+ options = list(
+ placeholder = "No topic selected, showing all indicators...",
+ plugins = list("clear_button")
+ )
+ ),
+ shiny::selectizeInput(
+ inputId = ns("indicator_name"),
+ label = "Indicator:",
+ choices = metric_names,
+ options = list(
+ placeholder = "Start typing or scroll to find an indicator...",
+ plugins = list("clear_button")
+ ),
+ )
+ )
+ )
+}
+
+
+#' Shiny Server Function for Handling the App Inputs with Synchronisation
+#'
+#' This function creates a Shiny server module for handling the app inputs
+#' and synchronising them across multiple pages.
+#' It observes the selected topic name and updates the choices for the
+#' indicator name based on the selected topic, and also updates the shared
+#' reactive values to keep the inputs in sync between pages.
+#'
+#' @param id A unique ID that identifies the server function.
+#' @param shared_values A `reactiveValues` object to store shared input values
+#' that can be accessed and modified across different modules.
+#' @return A list of reactive expressions for the app inputs, including
+#' the selected LA name, topic name, and indicator name.
+#'
+appInputsServer <- function(id,
+ shared_values,
+ topic_indicator_full) {
+ moduleServer(id, function(input, output, session) {
+ # Reactive value to store the previous LA name
+ previous_la_name <- reactiveVal(NULL)
+
+ # Debounce input values to prevent looping when inputs change quickly
+ debounced_la_name <- shiny::debounce(reactive(input$la_name), 150)
+ debounced_topic_name <- shiny::debounce(reactive(input$topic_name), 75)
+ debounced_indicator_name <- shiny::debounce(reactive(input$indicator_name), 150)
+
+ # Update Indicator dropdown for selected Topic
+ shiny::observeEvent(debounced_topic_name(),
+ {
+ # Save the currently selected indicator
+ current_indicator <- debounced_indicator_name()
+
+ # Determine the filter condition for Topic
+ topic_filter <- debounced_topic_name()
+
+ # Get indicator choices for selected topic
+ # Include all rows if no topic is selected or "All Topics" is selected
+ filtered_topic_bds <- topic_indicator_full |>
+ filter_by_topic("Topic", topic_filter) |>
+ pull_uniques("Measure")
+
+ # Ensure the current indicator stays selected
+ # Default to the first topic indicator if the current is not valid
+ selected_indicator <- if (current_indicator %in% filtered_topic_bds) {
+ current_indicator
+ } else {
+ filtered_topic_bds[1]
+ }
+
+ # Update the Indicator dropdown based on selected Topic
+ shiny::updateSelectizeInput(
+ session = session,
+ inputId = "indicator_name",
+ choices = filtered_topic_bds,
+ selected = selected_indicator
+ )
+
+ # Update the shared reactive value for the topic
+ shared_values$topic <- debounced_topic_name()
+ },
+ ignoreNULL = FALSE
+ )
+
+ # Prevent LA input from being empty by storing its previous value
+ shiny::observeEvent(debounced_la_name(), {
+ # Check if the LA name is NULL or empty
+ la_name <- debounced_la_name()
+
+ if ("" %notin% la_name && !is.null(la_name)) {
+ # Update the reactive value with the current valid input
+ previous_la_name(la_name)
+
+ # Synchronise the shared reactive value
+ shared_values$la <- la_name
+ }
+ })
+
+ # Set dynamic topic label
+ # (to display topic when not selected or all topics selected)
+ update_topic_label(
+ indicator_input = debounced_indicator_name,
+ topic_input = debounced_topic_name,
+ topic_indicator_data = topic_indicator_full,
+ topic_label_id = "topic_label"
+ )
+
+ # Observe and synchronise Indicator input changes
+ observeEvent(debounced_indicator_name(), {
+ shared_values$indicator <- debounced_indicator_name()
+ })
+
+ # Synchronise inputs across pages:
+ # LA
+ observe({
+ shiny::updateSelectizeInput(session, "la_name", selected = shared_values$la)
+ })
+ # Topic
+ observe({
+ shiny::updateSelectizeInput(session, "topic_name", selected = shared_values$topic)
+ })
+ # Indicator
+ observe({
+ shiny::updateSelectizeInput(session, "indicator_name", selected = shared_values$indicator)
+ })
+
+ # Return reactive settings
+ app_settings <- list(
+ la = reactive({
+ previous_la_name()
+ }),
+ topic = reactive({
+ debounced_topic_name()
+ }),
+ indicator = reactive({
+ debounced_indicator_name()
+ })
+ )
+
+ return(app_settings)
+ })
+}
+
+# nolint end
diff --git a/R/lait_modules/mod_create_own_charts.R b/R/lait_modules/mod_create_own_charts.R
index c4649a02..a92aee73 100644
--- a/R/lait_modules/mod_create_own_charts.R
+++ b/R/lait_modules/mod_create_own_charts.R
@@ -270,21 +270,24 @@ CreateOwnLineChartServer <- function(id, query, bds_metrics, covid_affected_data
if ("Message from tool" %in% colnames(create_own_data())) {
ggiraph::girafe(
ggobj = display_no_data_plot("No plot as not enough selections made."),
- width_svg = 8.5
+ width_svg = 8.5,
+ options = generic_ggiraph_options()
)
} else if (
chart_info$no_geogs() > 4
) {
ggiraph::girafe(
ggobj = display_no_data_plot(label = "No plot as too many Geographies selected."),
- width_svg = 8.5
+ width_svg = 8.5,
+ options = generic_ggiraph_options()
)
} else if (
chart_info$no_indicators() > 3
) {
ggiraph::girafe(
ggobj = display_no_data_plot(label = "No plot as too many Indicators selected."),
- width_svg = 8.5
+ width_svg = 8.5,
+ options = generic_ggiraph_options()
)
# Plot line chart
@@ -527,21 +530,24 @@ CreateOwnBarChartServer <- function(id, query, bds_metrics, covid_affected_data)
if ("Message from tool" %in% colnames(create_own_data())) {
ggiraph::girafe(
ggobj = display_no_data_plot("No plot as not enough selections made."),
- width_svg = 8.5
+ width_svg = 8.5,
+ options = generic_ggiraph_options()
)
} else if (
chart_info$no_geogs() > 4
) {
ggiraph::girafe(
ggobj = display_no_data_plot(label = "No plot as too many Geographies selected."),
- width_svg = 8.5
+ width_svg = 8.5,
+ options = generic_ggiraph_options()
)
} else if (
chart_info$no_indicators() > 3
) {
ggiraph::girafe(
ggobj = display_no_data_plot(label = "No plot as too many Indicators selected."),
- width_svg = 8.5
+ width_svg = 8.5,
+ options = generic_ggiraph_options()
)
# Plot chart
diff --git a/R/lait_modules/mod_create_own_inputs.R b/R/lait_modules/mod_create_own_inputs.R
index 18cefca9..49d04605 100644
--- a/R/lait_modules/mod_create_own_inputs.R
+++ b/R/lait_modules/mod_create_own_inputs.R
@@ -96,7 +96,12 @@ Create_MainInputsUI <- function(id) {
),
# Add selection (query) button
"Add selection" = div(
- style = "height: 100%; display: flex; justify-content: center; align-items: flex-end;",
+ style = "
+ height: 100%;
+ display: flex;
+ justify-content: center;
+ align-items: flex-end;
+ ",
shinyGovstyle::button_Input(
inputId = ns("add_query"),
label = "Add selections",
@@ -324,9 +329,9 @@ YearRangeServer <- function(id, bds_metrics, indicator_input, clear_selections)
shinyWidgets::updatePickerInput(
session = session,
inputId = "year_range",
- choices = "Please select an indicator first",
+ choices = "Select an indicator to see year range",
options = shinyWidgets::pickerOptions(
- noneSelectedText = "Select an indicator to see year range",
+ noneSelectedText = "Select an indicator...",
maxOptions = 2,
maxOptionsText = "Select and indicator",
size = "auto"
diff --git a/R/lait_modules/mod_create_own_table.R b/R/lait_modules/mod_create_own_table.R
index bc184d69..4cc91a53 100644
--- a/R/lait_modules/mod_create_own_table.R
+++ b/R/lait_modules/mod_create_own_table.R
@@ -1,854 +1,861 @@
-# nolint start: object_name
-#
-# Staging table ================================================================
-# Staging BDS ------------------------------------------------------------------
-# Filter the BDS for current user input selections
-# (used to create the staging table)
-#
-#' Staging BDS Server
-#'
-#' This function filters the BDS (Business Data Service) metrics based on
-#' user input selections to create a staging table. It filters for selected
-#' topic-indicator pairs, geographic groupings, and a specified year range.
-#'
-#' @param id A unique identifier for the Shiny module.
-#' @param create_inputs A list of reactive inputs generated by the main
-#' input module, including selected topic-indicator pairs.
-#' @param geog_groups A reactive expression providing the selected geographic
-#' groups based on user input.
-#' @param year_input A list containing reactive expressions for selected
-#' year range and available years choices.
-#' @param bds_metrics A data frame containing the business data service metrics
-#' used for filtering based on user selections.
-#' @return A reactive data frame that contains the filtered BDS metrics
-#' suitable for display in the staging table.
-#'
-StagingBDSServer <- function(id,
- create_inputs,
- geog_groups,
- year_input,
- bds_metrics) {
- moduleServer(id, function(input, output, session) {
- # Forcing module to react to change in year input (not best practice)
- observeEvent(year_input$range(), {
- year_input$range()
- })
-
- # Filter BDS for topic-indicator pairs in the selected_values reactive
- topic_indicator_bds <- reactive({
- req(length(create_inputs$indicator()) > 0)
- bds_metrics |>
- dplyr::filter(Measure %in% create_inputs$indicator())
- })
-
- # Now filter BDS for geographies and year range
- # Split from above so if indicator doesn't change then don't recompute
- staging_bds <- reactive({
- req(geog_groups(), topic_indicator_bds())
- # Filter by full geography inputs
- filtered_bds <- topic_indicator_bds() |>
- dplyr::filter(
- `LA and Regions` %in% geog_groups()
- )
-
- # Cleaning Years
- # Check if all years have consistent suffix
- consistent_str_years <- check_year_suffix_consistency(filtered_bds)
-
- # If not consistent suffix use the cleaned year cols (numeric years)
- if (!consistent_str_years) {
- filtered_bds <- filtered_bds |>
- dplyr::mutate(
- Years = Years_num
- )
- }
-
- # Apply the year range filter
- # If only one year selected then show just that year
- if (length(year_input$range()) == 1) {
- filtered_bds <- filtered_bds |>
- dplyr::filter(
- Years == year_input$range()[1]
- )
- } else if (length(year_input$range()) == 2) {
- filtered_bds <- filtered_bds |>
- dplyr::filter(
- Years >= year_input$range()[1],
- Years <= year_input$range()[2]
- )
- }
-
- # Return the user selection filtered data for staging table
- filtered_bds
- })
-
-
- # Return staging BDS
- staging_bds
- })
-}
-
-
-# Staging data -----------------------------------------------------------------
-#
-#' Staging Data Server
-#'
-#' This function builds a staging table for displaying filtered BDS metrics
-#' in a Shiny application. It incorporates statistical neighbour associations
-#' if selected and formats the data into a wide format for easier analysis.
-#'
-#' @param id A unique identifier for the Shiny module.
-#' @param create_inputs A list of reactive inputs generated by the main
-#' input module, including selected topic-indicator pairs.
-#' @param staging_bds A reactive expression providing the filtered BDS metrics
-#' based on user selections.
-#' @param region_names_bds A vector of names representing regions in the BDS.
-#' @param la_names_bds A vector of names representing local authorities in the BDS.
-#' @param stat_n_la A data frame containing statistical neighbour data for LAs.
-#' @return A reactive data frame that contains the formatted staging table
-#' ready for display in the Shiny app.
-#'
-StagingDataServer <- function(
- id, create_inputs, staging_bds, region_names_bds, la_names_bds, stat_n_la) {
- moduleServer(id, function(input, output, session) {
- # Make statistical neighbour association table available
- stat_n_association <- StatN_AssociationServer(
- "stat_n_association",
- create_inputs,
- la_names_bds,
- stat_n_la
- )
-
- # Build the staging table
- staging_table <- reactive({
- # Selected relevant cols
- # Coerce to wide format
- # (any new values created set to NaN so can be picked up as user created NAs)
- # Set regions and England as themselves for Region
- wide_table <- staging_bds() |>
- dplyr::select(
- `LA Number`, `LA and Regions`, Region, Topic,
- Measure, Years, Years_num, values_num, Values
- ) |>
- tidyr::pivot_wider(
- id_cols = c("LA Number", "LA and Regions", "Region", "Topic", "Measure"),
- names_from = Years,
- values_from = values_num,
- values_fill = NaN
- ) |>
- dplyr::mutate(Region = dplyr::case_when(
- `LA and Regions` %in% c("England", region_names_bds) ~ `LA and Regions`,
- TRUE ~ Region
- ))
-
- # Order columns (and sort year cols order)
- wide_table_ordered <- wide_table |>
- dplyr::select(
- `LA Number`, `LA and Regions`, Region,
- Topic, Measure,
- dplyr::all_of(sort_year_columns(wide_table))
- )
-
- # If SNs included, add SN LA association column
- # Multi-join as want to include an association for every row (even duplicates)
- if (isTRUE(create_inputs$la_group() == "la_stat_ns")) {
- wide_table_ordered <- wide_table_ordered |>
- dplyr::left_join(
- stat_n_association(),
- by = "LA and Regions",
- relationship = "many-to-many"
- ) |>
- dplyr::relocate(sn_parent, .after = "Measure") |>
- dplyr::rename("Statistical Neighbour Group" = "sn_parent")
- }
-
- # Staging table formatted and ready for output
- wide_table_ordered
- })
-
- # Return staging table
- staging_table
- })
-}
-
-
-# Staging table UI -------------------------------------------------------------
-# Simple reactable table inside a well div
-#
-#' Staging Table UI
-#'
-#' This function creates the user interface for the staging table, which
-#' displays the current selections in a well-styled format. The UI includes
-#' a header and a reactable output for rendering the staging data.
-#'
-#' @param id A unique identifier for the Shiny module.
-#' @return A div containing the UI elements for the staging table, including
-#' a header and a reactable output.
-#'
-StagingTableUI <- function(id) {
- ns <- NS(id)
-
- div(
- class = "well",
- style = "overflow-y: visible;",
- bslib::layout_column_wrap(
- h3(
- "Staging Table",
- create_tooltip_icon("Showing data from current selections")
- ),
- # Include empty divs so matches inputs above and add selections aligns
- div(),
- div(),
- # Add selections button
- Create_MainInputsUI("create_inputs")["Add selection"]
- ),
- bslib::card(
- with_gov_spinner(
- reactable::reactableOutput(ns("staging_table")),
- size = 0.5
- )
- )
- )
-}
-
-
-# Staging table Server ---------------------------------------------------------
-# Output a formatted reactable table of the staging data
-# Few error message table outputs for incorrect/ missing selections
-#
-#' Staging Table Server
-#'
-#' This function generates the server-side logic for the staging table, which
-#' renders a reactable table of the current selections. It handles error
-#' messages for incorrect or missing selections and formats the staging data
-#' for better readability. It filters the BDS data based on user inputs and
-#' prepares it for display.
-#'
-#' @param id A unique identifier for the Shiny module.
-#' @param create_inputs A list of reactive inputs generated by the main input
-#' module, including selected indicators and geography.
-#' @param region_names_bds A vector of names representing regions in the BDS.
-#' @param la_names_bds A vector of names representing local authorities in the BDS.
-#' @param stat_n_la A data frame containing statistical neighbour data for LAs.
-#' @param geog_groups A reactive expression that provides the selected geography
-#' groups based on user input.
-#' @param year_input A reactive expression providing the selected year range.
-#' @param bds_metrics A data frame containing the BDS metrics used for filtering.
-#' @return A reactable output for the staging table, displaying filtered BDS data
-#' or error messages based on user selections.
-StagingTableServer <- function(id,
- create_inputs,
- region_names_bds,
- la_names_bds,
- stat_n_la,
- geog_groups,
- year_input,
- bds_metrics) {
- moduleServer(id, function(input, output, session) {
- # Staging table reactable ouput
- output$staging_table <- reactable::renderReactable({
- # Display messages if there are incorrect selections
- if (length(create_inputs$indicator()) == 0 && is.null(geog_groups())) {
- return(reactable::reactable(
- data.frame(
- `Message from tool` = "Please add selections (above).",
- check.names = FALSE
- )
- ))
- } else if (length(create_inputs$indicator()) == 0) {
- return(reactable::reactable(
- data.frame(
- `Message from tool` = "Please add an indicator selection (above).",
- check.names = FALSE
- )
- ))
- } else if (is.null(geog_groups())) {
- return(reactable::reactable(
- data.frame(
- `Message from tool` = "Please add a geography selection (above).",
- check.names = FALSE
- )
- ))
- }
-
- # Filtering BDS for staging data
- staging_bds <- StagingBDSServer(
- "staging_bds",
- create_inputs,
- geog_groups,
- year_input,
- bds_metrics
- )
-
- # Build staging data
- staging_data <- StagingDataServer(
- "staging_data",
- create_inputs,
- staging_bds,
- region_names_bds,
- la_names_bds,
- stat_n_la
- )
-
- # Output table - formatting numbers, long text and page settings
- dfe_reactable(
- staging_data(),
- columns = utils::modifyList(
- format_num_reactable_cols(
- staging_data(),
- get_indicator_dps(staging_bds()),
- num_exclude = c("LA Number", "Topic", "Measure")
- ),
- list(
- set_custom_default_col_widths(
- Measure = set_min_col_width(90)
- ),
- # Truncates long cell values and displays hover with full value
- Measure = reactable::colDef(
- html = TRUE,
- cell = function(value, index, name) {
- truncate_cell_with_hover(text = value, tooltip = value)
- }
- )
- )
- ),
- defaultPageSize = 3,
- showPageSizeOptions = TRUE,
- pageSizeOptions = c(3, 5, 10, 25),
- compact = TRUE
- )
- })
- })
-}
-
-
-# Query table ==================================================================
-# Query data -------------------------------------------------------------------
-#
-#' Query Data Server
-#'
-#' This function manages the server-side logic for storing and displaying
-#' queries based on user selections. It allows users to add queries to a
-#' saved list and formats the data for display. The function maintains
-#' a reactive data structure that includes the selected topics, indicators,
-#' geography, and year range.
-#'
-#' @param id A unique identifier for the Shiny module.
-#' @param create_inputs A list of reactive inputs generated by the main input
-#' module, including selected indicators and geography.
-#' @param geog_groups A reactive expression that provides the selected geography
-#' groups based on user input.
-#' @param year_input A reactive expression providing the selected year range.
-#' @param staging_data A reactive expression that contains the staging data
-#' filtered based on user selections.
-#' @return A reactive value list containing the current queries and output data
-#' for display, including options for removing queries.
-#'
-QueryDataServer <- function(id,
- create_inputs,
- geog_groups,
- year_input,
- staging_data) {
- moduleServer(id, function(input, output, session) {
- # Reactive value "query" used to store query data
- # Uses lists to store multiple inputs (Geographies & Indicators)
- query <- reactiveValues(
- data = data.frame(
- Topic = I(list()),
- Indicator = I(list()),
- `LA and Regions` = I(list()),
- `Year range` = I(list()),
- `Click to remove query` = character(),
- `.query_id` = numeric(),
- check.names = FALSE
- ),
- output = data.frame(
- `LA Number` = character(),
- `LA and Regions` = character(),
- Region = character(),
- Topic = character(),
- Measure = character(),
- check.names = FALSE
- )
- )
-
- # When "Add table" button clicked - add query to saved queries
- observeEvent(create_inputs$add_query(),
- {
- # Check if anything selected
- req(length(geog_groups()) > 0 && length(create_inputs$indicator()) > 0)
-
- # Create a unique identifier for the new query (current no of queries + 1)
- new_q_id <- max(c(0, query$data$.query_id), na.rm = TRUE) + 1
-
- # Creating year range info
- # Get the range of available years
- available_years <- range(year_input$choices())
-
- # Define the year range info logic
- # None selected - all years - "All years (x to y)"
- # Range selected - "x to y"
- # One year selected - "x"
- year_range_display <- dplyr::case_when(
- length(year_input$range()) == 0 ~ paste0("All years (", available_years[1], " to ", available_years[2], ")"),
- length(year_input$range()) == 2 ~ paste(year_input$range()[1], "to", year_input$range()[2]),
- length(year_input$range()) == 1 ~ paste0("", year_input$range()[1])
- )
-
- # Evaluate user inputs for get_geog_selection()
- evaluated_inputs <- list(
- geog = create_inputs$geog(),
- la_group = create_inputs$la_group(),
- inc_regions = create_inputs$inc_regions(),
- inc_england = create_inputs$inc_england()
- )
-
- # Get selected Indicator Topics
- selected_topics <- staging_data() |>
- pull_uniques("Topic")
-
- # Create query information
- # Split multiple input choices with commas and line breaks
- # (indicator x, indicator y)
- # Assign the new query ID, selected topic-indicator pairs,
- # create the geog selections (special formatting for groupings),
- # year range (with logic from above) and the remove col
- new_query <- data.frame(
- .query_id = new_q_id,
- Topic = paste(selected_topics, collapse = ",
"),
- Indicator = paste(create_inputs$indicator(), collapse = ",
"),
- `LA and Regions` = paste(
- get_geog_selection(evaluated_inputs, la_names_bds, region_names_bds, stat_n_geog),
- collapse = ",
"
- ),
- `Year range` = year_range_display,
- `Click to remove query` = "Remove",
- check.names = FALSE
- )
-
- # Append new query to the existing queries
- query$data <- query$data |>
- rbind(new_query)
-
- # Appending the data of the new query to the output table
- # Adding new query ID to staging data
- # (so remove button also removes relevant data from output table)
- query_output <- query$output
- staging_to_append <- staging_data()
- staging_to_append$.query_id <- new_q_id
- consistent_staging_final_yrs <- data.frame(
- Years = c(
- colnames(query_output)[grepl("^\\d{4}", colnames(query_output))],
- colnames(staging_to_append)[grepl("^\\d{4}", colnames(staging_to_append))]
- )
- ) |> check_year_suffix_consistency()
-
- # If not consistent suffixes then clean both dfs year cols
- if (!consistent_staging_final_yrs && nrow(query_output) > 0) {
- query_output <- rename_columns_with_year(query_output)
- staging_to_append <- rename_columns_with_year(staging_to_append)
- }
-
- # Get all years across both dfs
- all_year_columns <- union(
- grep("^\\d{4}", names(query_output), value = TRUE),
- grep("^\\d{4}", names(staging_to_append), value = TRUE)
- )
-
- # Add the new (missing) years onto the existing dfs with values as NaN
- # This is so that they can be coded as "-" in the table
- # Saved queries
- if (nrow(query_output) > 0) {
- for (col in setdiff(all_year_columns, names(query_output))) {
- query_output[[col]] <- NaN
- }
- }
-
- # New query
- if (nrow(staging_to_append) > 0) {
- for (col in setdiff(all_year_columns, names(staging_to_append))) {
- staging_to_append[[col]] <- NaN
- }
- }
-
- # Combine query tables for final table output
- query$output <- rbind(query_output, staging_to_append)
- },
- ignoreInit = TRUE
- )
-
- query
- })
-}
-
-
-# Query Table UI ---------------------------------------------------------------
-#
-#' Query Table UI
-#'
-#' This function creates the user interface for displaying a summary of
-#' saved queries in a well-styled format. It includes a reactable table
-#' output to present the user's selections.
-#'
-#' @param id A unique identifier for the Shiny module.
-#' @return A UI element that displays a summary of selections in a
-#' reactable table format.
-#'
-QueryTableUI <- function(id) {
- ns <- NS(id)
-
- div(
- class = "well",
- style = "overflow-y: visible;",
- h3("Summary of Selections"),
- bslib::card(
- with_gov_spinner(
- reactable::reactableOutput(ns("query_table")),
- size = 0.5
- )
- )
- )
-}
-
-# Query Table Server -----------------------------------------------------------
-# Renders the query table and manages removal actions
-#
-#' Query Table Server
-#'
-#' This function handles the server-side logic for rendering the query
-#' table and managing the removal of saved queries. It displays the
-#' current queries and allows users to remove specific entries.
-#'
-#' @param id A unique identifier for the Shiny module.
-#' @param query A reactive list containing the current query data, including
-#' saved queries and output for display.
-#' @return A reactive value list that updates when queries are added or
-#' removed, reflecting the current state of the query data.
-#'
-QueryTableServer <- function(id, query) {
- moduleServer(id, function(input, output, session) {
- # Display message if there are no saved selections
- output$query_table <- reactable::renderReactable({
- req(nrow(query$data))
- if (nrow(query$data) == 0) {
- return(reactable::reactable(
- data.frame(`Message from tool` = "No saved selections.", check.names = FALSE)
- ))
- }
-
- # Output table - Allow html (for
),
- # add the JS from reactable.extras::button_extra() for remove button
- # Show only unique topics and remove the query ID col
- dfe_reactable(
- query$data,
- columns = list(
- Indicator = html_col_def(),
- `LA and Regions` = html_col_def(),
- `Click to remove query` = reactable::colDef(
- cell = reactable::JS(
- "function(cellInfo) {
- const buttonId = 'query_table-remove-' + cellInfo.row['.query_id'];
- console.log('Generated button ID:', buttonId); // Confirm buttonId in console
- return React.createElement(ButtonExtras, {
- id: buttonId,
- label: 'Remove',
- uuid: cellInfo.row['.query_id'],
- column: cellInfo.column.id,
- class: 'govuk-button--warning',
- className: 'govuk-button--warning'
- }, cellInfo.index);
- }"
- )
- ),
- Topic = html_col_def(),
- .query_id = reactable::colDef(show = FALSE)
- ),
- defaultPageSize = 5,
- showPageSizeOptions = TRUE,
- pageSizeOptions = c(5, 10, 25),
- compact = TRUE
- )
- })
-
- # Remove query button logic
- observe({
- req(nrow(query$data))
-
- # Create button observers for each row using the query ID
- lapply(query$data$.query_id, function(q_id) {
- # Create matching query ID for each remove button
- remove_button_id <- paste0("remove-", q_id)
-
- # Observe the button click
- observeEvent(input[[remove_button_id]],
- {
- # Remove the corresponding row (query) from query$data using the query ID
- query$data <- query$data[query$data$.query_id != q_id, , drop = FALSE]
-
- # Also remove the corresponding rows from query$output
- query$output <- query$output[query$output$.query_id != q_id, , drop = FALSE]
-
- # If no rows (queries) left then also remove the years cols
- # This is so that if a user wants a range of years next
- # the legacy years aren't still there
- if (nrow(query$output) == 0) {
- query$output <- query$output |>
- dplyr::select(
- `LA Number`,
- `LA and Regions`,
- Region,
- Topic,
- Measure,
- .query_id
- )
- }
- },
- ignoreInit = TRUE
- )
- })
- })
-
- # Output updated query (which is up-to-date with any removed rows)
- query
- })
-}
-
-
-# Create Own Table =============================================================
-# Create Own Data --------------------------------------------------------------
-#
-#' Create Own Data Server
-#'
-#' This function processes saved queries and generates a cleaned final
-#' table output for display. It checks for year suffix consistency and
-#' adjusts the column names accordingly. If there are no saved queries,
-#' it returns a message indicating this.
-#'
-#' @param id A unique identifier for the Shiny module.
-#' @param query A reactive list containing the current query data, including
-#' saved queries and output for display.
-#' @param bds_metrics A data frame containing metrics related to the BDS,
-#' which is used to verify year suffix consistency.
-#' @return A reactive data frame containing the cleaned final output table
-#' with correctly formatted year columns and relevant information.
-#'
-CreateOwnDataServer <- function(id, query, bds_metrics) {
- moduleServer(id, function(input, output, session) {
- # Building data for the output of all saved queries
- clean_final_table <- reactive({
- req(query$data)
-
- # Check if there are any saved queries
- if (nrow(query$data) == 0) {
- return(
- data.frame(
- `Message from tool` = "No saved selections.",
- check.names = FALSE
- )
- )
- }
-
- # Remove columns that contain only NaN values
- # (aka user removed query that was including these years so no need to display them now)
- query_output_clean <- query$output[, !sapply(query$output, function(x) all(is.nan(x)))]
-
- # Logic to reset the year cols to have year suffixes if they match
- # (As they may have been cleaned from the code logic at end of the new query chunk)
- # Determine if output indicators share year suffix consistency
- output_indicators <- query_output_clean |> pull_uniques("Measure")
- share_year_suffix <- bds_metrics |>
- dplyr::filter(Measure %in% output_indicators) |>
- check_year_suffix_consistency()
-
- # Reapply year suffixes to columns if needed
- if (share_year_suffix) {
- years_dict <- bds_metrics |>
- dplyr::filter(Measure %in% output_indicators) |>
- dplyr::distinct(Years, Years_num)
-
- # Replace numeric year columns with the corresponding suffix
- new_col_names <- colnames(query_output_clean) |>
- vapply(function(col) {
- if (col %in% years_dict$Years_num) {
- return(years_dict$Years[match(col, years_dict$Years_num)])
- } else {
- return(col)
- }
- }, character(1))
-
- colnames(query_output_clean) <- new_col_names
- }
-
- # Final query output table with ordered columns (SN parent if selected)
- # and sorted year columns
- query_output_clean |>
- dplyr::select(
- `LA Number`, `LA and Regions`,
- Region, Topic, Measure,
- tidyselect::any_of("Statistical Neighbour Group"),
- dplyr::all_of(sort_year_columns(query_output_clean))
- )
- })
-
- # Return data ready to render as output of Create Own Table
- clean_final_table
- })
-}
-
-
-# Create Own BDS ---------------------------------------------------------------
-#
-#' Create Own BDS Server
-#'
-#' This function filters the BDS metrics based on the topic-indicator pairs
-#' present in the final output table. It returns a reactive data frame
-#' containing only the relevant entries from the BDS that match the specified
-#' selections.
-#'
-#' @param id A unique identifier for the Shiny module.
-#' @param create_own_table A reactive expression that returns the final output
-#' table containing selected topic-indicator pairs.
-#' @param bds_metrics A data frame containing the full BDS metrics to be
-#' filtered based on the selections.
-#' @return A reactive data frame containing the filtered BDS metrics based
-#' on the selected topic-indicator pairs from the final output table.
-#'
-CreateOwnBDSServer <- function(id, create_own_table, bds_metrics) {
- moduleServer(id, function(input, output, session) {
- # Filtering BDS for all topic-indicator pairs in the final output table
- # (The filtered_bds only has the staging topic-indicator pairs)
- final_filtered_bds <- reactive({
- output_table_filters <- create_own_table() |>
- dplyr::distinct(`LA and Regions`, Topic, Measure)
-
- bds_metrics |>
- dplyr::semi_join(
- output_table_filters,
- by = c("LA and Regions", "Topic", "Measure")
- )
- })
-
- final_filtered_bds
- })
-}
-
-
-# Create Own Table UI ----------------------------------------------------------
-#
-#' Create Own Table UI
-#'
-#' This function generates the user interface for displaying the output table
-#' that shows all saved selections, along with a download section for exporting
-#' the table in various file formats.
-#'
-#' @param id A unique identifier for the Shiny module, used for namespacing.
-#' @return A UI component consisting of a well containing the output table and
-#' download options.
-#'
-CreateOwnTableUI <- function(id) {
- ns <- NS(id)
-
- div(
- class = "well",
- style = "overflow-y: visible;",
- h3(
- "Output Table",
- create_tooltip_icon(
- '