Table of contents

Table of content for chapter 10

Chapter section list

There are three key techniques for creating dynamic user interfaces:

  • Using the update family of functions to modify parameters of input controls.
  • Using tabsetPanel() to conditionally show and hide parts of the user interface.
  • Using uiOutput() and renderUI() to generate selected parts of the user interface with code.

10.1 Updating inputs

We’ll begin with a simple technique that allows you to modify an input after it has been created: the update family of functions. Every input control, e.g. textInput(), is paired with an update function, e.g. updateTextInput(), that allows you to modify the control after it has been created.

The key idea to modify an input is to use observeEvent() to trigger the corresponding update function whenever the input changes. (For the observeEvent() function see and .)

R Code 10.1 : Updating the slider whenever the the minimum or maximum input changes

Loading...




10.1.1 Simple uses

10.1.1.1 Reset button

The simplest uses of the update functions are to provide small conveniences for the user. For example, maybe you want to make it easy to reset parameters back to their initial value.

R Code 10.2 : Updating controls with reset button

Loading...




10.1.1.2 Button changed functionality

A similar application is to tweak the text of an action button so you know exactly what it’s going to do.

R Code 10.3 : Updating button text if it has changed functionality

Loading...




10.1.2 Hierarchical select boxes

A more complicated, but particularly useful, application of the update functions is to allow interactive drill down across multiple categories. I’ll illustrate their usage with some imaginary data for a sales dashboard.

Resource 10.1 : Kaggle — An online community platform for data scientists and machine learning enthusiasts

This section uses Sample Sales Data, a well-documented imaginary dataset provided by Kaggle for education, training and research. To download the dataset from Kaggle you have to register. But you can also download the Sample Sales Data as .csv file from the GitHub repo of Mastering Shiny or alternatively my own version of the dataset.


Kaggle is one of the largest hosting platforms used by data scientists and machine learning enthusiasts globally. It allows users to collaborate with other users, find and publish datasets, use GPU integrated notebooks, and compete with other data scientists to solve data science challenges.

Founded in 2010 by Anthony Goldbloom and Jeremy Howard, Kaggle was acquired by Google in 2017.

R Code 10.4 : Load and display sales dataset

Code
sales <- vroom::vroom("data/sales_data_sample.csv", col_types = list(), na = "")
sales  |> 
  dplyr::select(TERRITORY, CUSTOMERNAME, ORDERNUMBER, dplyr::everything()) |> 
  dplyr::arrange(ORDERNUMBER)
#> # A tibble: 2,823 × 25
#>    TERRITORY CUSTOMERNAME  ORDERNUMBER QUANTITYORDERED PRICEEACH ORDERLINENUMBER
#>    <chr>     <chr>               <dbl>           <dbl>     <dbl>           <dbl>
#>  1 NA        Online Dieca…       10100              30     100                 3
#>  2 NA        Online Dieca…       10100              50      67.8               2
#>  3 NA        Online Dieca…       10100              22      86.5               4
#>  4 NA        Online Dieca…       10100              49      34.5               1
#>  5 EMEA      Blauer See A…       10101              25     100                 4
#>  6 EMEA      Blauer See A…       10101              26     100                 1
#>  7 EMEA      Blauer See A…       10101              45      31.2               3
#>  8 EMEA      Blauer See A…       10101              46      53.8               2
#>  9 NA        Vitachrome I…       10102              39     100                 2
#> 10 NA        Vitachrome I…       10102              41      50.1               1
#> # ℹ 2,813 more rows
#> # ℹ 19 more variables: SALES <dbl>, ORDERDATE <chr>, STATUS <chr>,
#> #   QTR_ID <dbl>, MONTH_ID <dbl>, YEAR_ID <dbl>, PRODUCTLINE <chr>, MSRP <dbl>,
#> #   PRODUCTCODE <chr>, PHONE <chr>, ADDRESSLINE1 <chr>, ADDRESSLINE2 <chr>,
#> #   CITY <chr>, STATE <chr>, POSTALCODE <chr>, COUNTRY <chr>,
#> #   CONTACTLASTNAME <chr>, CONTACTFIRSTNAME <chr>, DEALSIZE <chr>

For this demo, I’m going to focus on a natural hierarchy in the data:

  • Each territory contains customers.
  • Each customer has multiple orders.
  • Each order contains rows.

I want to create a user interface where you can:

  • Select a territory to see all customers.
  • Select a customer to see all orders.
  • Select an order to see the underlying rows.

The essence of the UI is simple: I’ll create three select boxes and one output table. The choices for the customername and ordernumber select boxes will be dynamically generated, so I set choices = NULL.

In the server function, there is th following top-down procedure to follow:

Procedure 10.1 : Replicating the hierarchical data structure in the server function

  1. Create a reactive, territory(), that contains the rows from sales that match the selected territory.
  2. Whenever territory() changes, update the list of choices in the input$customername select box.
  3. Create another reactive, customer(), that contains the rows from territory() that match the selected customer.
  4. Whenever customer() changes, update the list of choices in the input$ordernumber select box.
  5. Display the selected orders in output$data.

Code Collection 10.1 : Hierarchical select boxes

R Code 10.5 : Hierarchical select boxes: Small demo

Listing / Output 10.1: Small demo of hierarchical select boxes
Loading...




R Code 10.6 : Hierarchical select boxes: More fleshed out

Listing / Output 10.2: Hierarchical select boxes: more fleshed out demo
Watch out! 10.1: Couldn’t load data without user input

To get a version that works with Shiny and shinylive-r at the same time, I had to provide fileInput(). This is suboptimal as the user has to find and select the file to get the intended data for playing around.

It was easy to find a solution for Shiny: Just load the data before the ui/server part into an R object and then reference it from the server() function. I couldn’t manage the same result for shinylive-r. Local references, relative URL and non-HTTPS URL generate error. The only possible way seems to doanload the data via HTTPS. I tried but didn’t succeed. I asked for help via the Posit Community and are currently waiting for answers.

My solution with shinylive-r choosing the file by the user is in and

In the meanwhile I received an answer. I commit a silly error providing the wrong URL for downloading the raw data. Instead of the GitHub reference https://raw.githubusercontent.com/petzi53/…I used https://github.com/petzi53/…. This worked in the Shiny app mode, because there the app is using {curl}, a package that is not available in shinylive-r where the fallback is using the URL.

What follows are the original code chunks from “Mastering Shiny” to demonstrate that uploading the file via an URL is working with shinylive-r.

R Code 10.7 : Hierarchical select boxes with external file uploaded programmatically

Listing / Output 10.3: Hierarchical select boxes with external file uploaded programmatically

A workaround was to provide a file upload control. But even to create a workable demo took me hours. It was a bitter experience to learn that I still didn’t understand quite well how reactives work. As the debugger browser() did not work, it was finally for me quite helpful to install print() outputs in every reactive function to see what happens. In the end I solved this problem with three important changes:

  1. Creating a reactive value with sales <- reactiveVal().
  2. Adding an observeEvent() function which fulfilled several tasks:
    • Loading the data after fileInput() was active.
    • Assigning the file to the reactive value sales.
    • Updating with the data the choice of the territory selectInput() function. (Previously I had to set the UI choices for territory to NULL to prevent an error because the data was not available at start up.)
  3. The reference to the previous sales object had to be changed to the reactive function sales(). Additional had I to add req(input$upload) to prevent that the reactive territory function is called immediately after start up without available data.

What follows are the original code chunks from “Mastering Shiny” with one change: Instead to get the sales data locally I uploaded it via an URL.

In the meanwhile I received an answer. I commit a silly error providing the wrong URL for downloading the raw data.

What follows are the original code chunks from “Mastering Shiny” with a tiny change: Instead to get the sales data locally I uploaded it via an URL.

Code Collection 10.2 : Hierarchical select boxes

R Code 10.8 : Hierarchical select boxes: Small demo

Listing / Output 10.4: Small demo of hierarchical select boxes

R Code 10.9 : Hierarchical select boxes: More fleshed out

Listing / Output 10.5: Hierarchical select boxes: more fleshed out demo

10.1.3 Freezing reactive inputs

Sometimes hierarchical selections can briefly create an invalid set of inputs, leading to a flicker of undesirable output.

R Code 10.10 : Flicker of undesirable output

You’ll notice that when you switch datasets the summary output will briefly flicker. That’s because updateSelectInput() only has an effect after all outputs and observers have run, so there’s temporarily a state where you have dataset B and a variable from dataset A, so that the output contains summary(NULL).

You can resolve this problem by “freezing” the input with freezeReactiveValue(). This ensures that any reactives or outputs that use the input won’t be updated until the next full round of invalidation. It’s good practice to always use it when you dynamically change an input value.

R Code 10.11 : Freezing input with freezeReactiveValaue()

10.1.4 Circular references

Using an update function to modify value is no different to the user modifying the value by clicking or typing. That means an update function can trigger reactive updates in exactly the same way that a human can. This means that you are now stepping outside of the bounds of pure reactive programming, and you need to start worrying about circular references and infinite loops.


library(shiny)

ui <- fluidPage(
    actionButton("action", "start"),
    numericInput("n", "n", 0)
)
server <- function(input, output, session) {
    observeEvent(input$n,
                 updateNumericInput(inputId = "n", value = input$n + 1)
    )
}

shinyApp(ui, server)

For example, take the simple app above. It contains a single input control and an observer that increments its value by one. Every time updateNumericInput() runs, it changes input$n, causing updateNumericInput() to run again, so the app gets stuck in an infinite loop constantly increasing the value of input$n.

To prevent that the circular reference runs all the time I had to add a start and a stop button. This was a welcome training occasion for me. I used as help the article Using Action Buttons.

R Code 10.12 : Demonstration of circular references

10.1.6 Exercises

10.1.6.1 Update date

Task description: Complete the user interface below with a server function that updates input$date so that you can only select dates in input$year.

Exercise 10.1 : Update date input to limit the choices for the selected year

10.1.6.2 Update county

Task description: Complete the user interface below with a server function that updates input$county choices based on input$state. For an added challenge, also change the label from “County” to “Parish” for Louisiana and “Borough” for Alaska.

Exercise 10.2 : Update county by chose state

10.1.6.3 Update country

Task description: Complete the user interface below with a server function that updates input$country choices based on the input$continent. Use output$data to display all matching rows.

Exercise 10.3 : Updating country by chosen state

10.1.6.4 Updating all

Task description: Extend the previous app so that you can also choose to select all continents, and hence see all countries. You’ll need to add "(All)" to the list of choices, and then handle that specially when filtering.

Exercise 10.4 : Show all countries when continent “All” was chosen

10.1.6.5 Circular reference

Task description: What is at the heart of the problem described at https://community.rstudio.com/t/29307?

It is a circular reference because the different numericInput() are mutually dependent to each other. The first observeEvent() listens for input$A and changes input$B. But the second observeEvent() which listens for input$B changes input$A and therefore triggers the first observeEvent() creating an endless circular reference.

10.2 Dynamic visibility

R Code 10.14 : Dynamic visibility basics

There are two main ideas here:

  • Use tabset panel with hidden tabs.
  • Use updateTabsetPanel() to switch tabs from the server.

10.2.1 Conditional UI

R Code 10.15 : Conditional UI

Note that the value of (e.g.) input$mean is independent of whether or not its visible to the user. The underlying HTML control still exists; you just can’t see it.

10.2.2 Wizard interface

You can also use this idea to create a “wizard”, a type of interface that makes it easier to collect a bunch of information by spreading it across multiple pages. Here we embed action buttons within each “page”, making it easy to go forward and back.

R Code 10.16 : Numbered R Code Title

Note the use of the switch_page() function to reduce the amount of duplication in the server code.

10.2.3 Exercises

10.2.3.1 Show additonal controls

Task description: Use a hidden tabset to show additional controls only if the user checks an “advanced” check box.

Exercise 10.5 : Show additional controls after selecting a check box

10.2.3.2 Choose geom

Create an app that plots ggplot(diamonds, aes(carat)) but allows the user to choose which geom to use: geom_histogram(), geom_freqpoly(), or geom_density(). Use a hidden tabset to allow the user to select different arguments depending on the geom: geom_histogram() and geom_freqpoly() have a binwidth argument; geom_density() has a bw argument.

Exercise 10.6 : Choose geom for plotting

R Code 10.17 : Choose geom for plotting ({shiny} variant)

Listing / Output 10.6: Choose geom for plotting diamonds data. User interface with {shiny}

R Code 10.18 : Choose geom for plotting ({shiny} variant)

Listing / Output 10.7: Choose geom for plotting diamonds data. User interface with {bslib}
Note 10.1: Limiting the user input to appropriate number ranges or choices
  • To get an adequate number range for geom_histogram() and geom_freqpoly() I had to experiment with the data.
  • For the bw argument of the density distribution I had to read (and understand!) the docs about kernel density estimation.

Bandwidth refers to a parameter that controls the smoothness of the estimated density function. It determines the width of the kernel used in the estimation process. Specifically, the bandwidth is the standard deviation of the kernel, which influences how much each data point contributes to the overall density estimate. (What does bandwidth mean? and Wikipedia about bandwidth selection)

To experiment for choosing an appropriate bandwidth is important because different bandwidths may provide you with very diverging impressions of the underlying distribution. For more details, read: The importance of kernel density estimation bandwidth.

Caution 10.1: Problem with loalization of decimal separator

Surprisingly I got commas as decimal separator which I couldn’t change to dots. I tried it with different approaches:

  • Setting the output of decimal separator to a dot using the R command options(OutDec="."). Does not help because it is already the default. (Option settings)
  • Setting a different locale for specific Shiny apps with Sys.setlocale("LC_ALL","C") or Sys.setlocale("LC_ALL","en_US.UTF-8") (Setting a different locale for specific Shiny apps).
  • Using a textInput() and convert it to a number in R with as.numeric(input$mynumber). You could also change the comma separator to a dot with as.numeric(sub(",", ".", input$mynumber)) But the problem with textInput() is that the user could could type in any string, not just a number. (Shiny apps_issue with decimal point/comma) — So this approach would require a more complex workaround using input validation, maybe with the {shinyFeedback} package.

Instead of numericInput() I used as a workaround sliderInput()

The HTML5 input type=number is inadequate from the localization point of view, due to both the definition and the implementations. It is meant to be localized but as per the locale of the browser, which you cannot set or even know as a designer/author. (Localization of input type number)

10.2.3.3 Choose geom advanced

10.3 Creating UI with code

The update functions only allow you to change existing inputs, and a tabset only works if you have a fixed and known set of possible combinations. Sometimes you need to create different types or numbers of inputs (or outputs), depending on other inputs.

There are two parts to this solution:

  • uiOutput() inserts a placeholder in your ui. This leaves a “hole” that your server code can later fill in.
  • renderUI() is called within server() to fill in the placeholder with dynamically generated UI.

10.3.1 Getting started

Code Collection 10.3 : Creating UI with code

R Code 10.19 : Creating UI with code: basics

R Code 10.20 : Creating UI with code with values transferred

The use of isolate() in the improved version is important. We’ll come back to what it does in , but here it ensures that we don’t create a reactive dependency that would cause this code to re-run every time input$dynamic changes (which will happen whenever the user modifies the value). We only want it to change when input$type or input$label changes.

Watch out! 10.2

The book example does not work. It raises the error:

In sliderInput(): min, max, and value cannot be NULL, NA, or empty.

The problem is that at app initialization, input$dynamic is NULL which rise the error. It could be fixed with value=0 (like default) when input$dynamic is NULL.

Before I looked at the GitHub Repo of “Mastering Shiny” I tried to solve the problem with reserving slider and numeric input to get the latter as the first (default) option. A numeric input does not raise an error. But this hack does not work if the user changes immediately into slider mode, without to input a numeric value.

This problem is already reported on the repo website of Mastering Shiny. The author of issues 515 proposed also a solution with the % || % null coalescing operator I have never heard about. x % || % y is a 1-line function and an alternative way to call if (is.null(x)) y else x.

(Hadley uses this trick too to solve the same problem in the next section.)

There are two downsides creating UI with code:

  • Relying on it too much can create a laggy UI. For good performance, strive to keep fixed as much of the user interface as possible.
  • When you change controls, you lose the currently selected value. Maintaining existing state is one of the big challenges of creating UI with code. This is one reason that selectively showing and hiding UI is a better approach if it works for you — because you’re not destroying and recreating the controls, you don’t need to do anything to preserve the values.

However, in many cases, we can fix the problem by setting the value of the new input to the current value of the existing control.

10.3.2 Multiple controls

Dynamic UI is most useful when you are generating an arbitrary number or type of controls. That means that you’ll be generating UI with code, and I recommend using functional programming for this sort of task.

Imagine that you’d like users to be able to supply their own color palette. They’ll first specify how many colors they want, and then supply a value for each color. The ui is pretty simple: we have

  • a numericInput() that controls the number of inputs,
  • a uiOutput() where the generated text boxes will go, and
  • a textOutput() that demonstrates that we’ve plumbed everything together correctly.

Code Collection 10.4 : Creating UI with multiple controls

R Code 10.21 : Creating UI with multiple controls

R Code 10.22 : Numbered R Code Title

Hadley uses here purrr::map() and purrr::reduce(), but you could certainly do the same with the base lapply() and Reduce() functions. If you’re not familiar with the map() and reduce() of functional programming, you might want to take a brief detour to read Functional programming before continuing. We’ll also come back to this idea in .

Although the server function is short it contains some big ideas:

  • It uses a reactive, col_names(), to store the names of each of the colour inputs the user is about to generate.
  • Using purrr::map() to create a list of textInput()s, one each for each name in col_names(). renderUI() then takes this list of HTML components and adds it to UI.
  • Using a new trick to access the values of the input values. So far we’ve always accessed the components of input with $, e.g. input$col1. But here we have the input names in a character vector, like var <- "col1". $ no longer works in this scenario, so we need to swich to [[, i.e. input[[var]].
  • Using purrr::map_chr() to collect all values into a character vector, and display that in output$palette. Unfortunately there’s a brief period, just before the new inputs are rendered by the browser, where their values are NULL. This causes purrr::map_chr() to error, which we fix by using the handy %||% function: it returns the right-hand side whenever the left-hand side is NULL.

If you run this app, you’ll discover a really annoying behavior: whenever you change the number of colors, all the data you’ve entered disappears. We can fix this problem by using the same technique as before: setting value to the (isolated) current value. We’ll also tweak the appearance in the improved version to look a little nicer, including displaying the selected colors in a plot.

10.3.3 Dynamic filtering

The last example in this chapter is to create an app that lets you dynamically filter any data frame. Each numeric variable will get a range slider and each factor variable will get a multi-select, so (e.g.) if a data frame has three numeric variables and two factors, the app will have three sliders.

Code Collection 10.5 : Creating UI for dynamic filtering of data

R Code 10.23 : Creating UI for dynamic filtering of data: First take

Listing / Output 10.8: Creating UI for dynamic filtering of data: First take

R Code 10.24 : Creating UI for dynamic filtering of data: Second version

Listing / Output 10.9: Creating UI for dynamic filtering of data: Improved

R Code 10.25 : Creating UI for dynamic filtering of data: Third version

Listing / Output 10.10: Creating UI for dynamic filtering of data: Generalized

Basic version

  1. make_ui(): Creating this app we start with a function make_ui() that creates the UI for a single variable. It’ll return a range slider for numeric inputs, a multi-select for factor inputs, and NULL (nothing) for all other types.
  2. filter_var(): The next step is to write the server side equivalent of make-ui(): filter_var() takes the variable and value of the input control, and returns a logical vector saying whether or not to include each observation. Using a logical vector makes it easy to combine the results from multiple columns.

Improved version

You might notice that the app only works with three columns. We can make it work with all the columns by using a little functional programming:

  • In ui() use purrr::map() to generate one control for each variable.
  • In server(), use purrr::map() to generate the selection vector for each variable.
  • Then use reduce() to take the logical vector for each variable and combine them into a single logical vector by &-ing each vector together.

Again, don’t worry too much if you don’t understand exactly what’s happening here. The main take away is that once you master functional programming, you can write very succinct code that generate complex, dynamic apps.

Generalized version

From there, it’s a simple generalization to work with any data frame. illustrates it using the data frames in the {datasets} package, but you can easily imagine how you might extend this to user uploaded data.

10.3.4 Dialog boxes

Before we finish up, there is another but related technique to mention: dialog boxes.

You’ve seen them already in , where the contents of the dialog was a fixed text string. But because modalDialog() is called from within the server function, you can actually dynamically generate content in the same way as renderUI(). This is a useful technique to have in your back pocket if you want to force the user to make some decision before continuing on with the regular app flow.

10.3.5 Exercises

10.4 Glossary Entries

term definition
CSV Text files where the values are separated with commas (Comma Separated Values = CSV). These files have the file extension .csv
Kaggle Kaggle is one of the largest hosting platforms used by data scientists and machine learning enthusiasts globally. It allows users to collaborate with other users, find and publish datasets, use GPU integrated notebooks, and compete with other data scientists to solve data science challenges. Founded in 2010 by Anthony Goldbloom and Jeremy Howard, Kaggle was acquired by Google in 2017.
Kernel The basic idea of a kernel densitiy estimate is to have a set of data to predict future results. The kernel idea is to create a similarity function (called a kernel function) between any two sets of inputs. For any new set of inputs, we predict by taking a weighted average of past results, weighted by the similarity of the past inputs to the current inputs. So in a essence a kernel is a function used to measure similarity between data points in a transformed feature space. Intuitively, you can think of a kernel as a way to compute the "distance" or "similarity" between points without explicitly mapping them into that higher-dimensional space.

Session Info

Session Info

Code
sessioninfo::session_info()
#> ─ Session info ───────────────────────────────────────────────────────────────
#>  setting  value
#>  version  R version 4.5.1 (2025-06-13)
#>  os       macOS Sequoia 15.5
#>  system   aarch64, darwin20
#>  ui       X11
#>  language (EN)
#>  collate  en_US.UTF-8
#>  ctype    en_US.UTF-8
#>  tz       Europe/Vienna
#>  date     2025-07-17
#>  pandoc   3.7.0.2 @ /opt/homebrew/bin/ (via rmarkdown)
#>  quarto   1.8.4 @ /usr/local/bin/quarto
#> 
#> ─ Packages ───────────────────────────────────────────────────────────────────
#>  package      * version    date (UTC) lib source
#>  archive        1.1.12     2025-03-20 [1] CRAN (R 4.5.0)
#>  bit            4.6.0      2025-03-06 [1] CRAN (R 4.5.0)
#>  bit64          4.6.0-1    2025-01-16 [1] CRAN (R 4.5.0)
#>  cli            3.6.5      2025-04-23 [1] CRAN (R 4.5.0)
#>  commonmark     2.0.0      2025-07-07 [1] CRAN (R 4.5.0)
#>  crayon         1.5.3      2024-06-20 [1] CRAN (R 4.5.0)
#>  curl           6.4.0      2025-06-22 [1] CRAN (R 4.5.0)
#>  dichromat      2.0-0.1    2022-05-02 [1] CRAN (R 4.5.0)
#>  digest         0.6.37     2024-08-19 [1] CRAN (R 4.5.0)
#>  dplyr          1.1.4      2023-11-17 [1] CRAN (R 4.5.0)
#>  evaluate       1.0.4      2025-06-18 [1] CRAN (R 4.5.0)
#>  farver         2.1.2      2024-05-13 [1] CRAN (R 4.5.0)
#>  fastmap        1.2.0      2024-05-15 [1] CRAN (R 4.5.0)
#>  generics       0.1.4      2025-05-09 [1] CRAN (R 4.5.0)
#>  glossary     * 1.0.0.9003 2025-06-08 [1] local
#>  glue           1.8.0      2024-09-30 [1] CRAN (R 4.5.0)
#>  htmltools      0.5.8.1    2024-04-04 [1] CRAN (R 4.5.0)
#>  htmlwidgets    1.6.4      2023-12-06 [1] CRAN (R 4.5.0)
#>  jsonlite       2.0.0      2025-03-27 [1] CRAN (R 4.5.0)
#>  kableExtra     1.4.0      2024-01-24 [1] CRAN (R 4.5.0)
#>  knitr          1.50       2025-03-16 [1] CRAN (R 4.5.0)
#>  lifecycle      1.0.4      2023-11-07 [1] CRAN (R 4.5.0)
#>  litedown       0.7        2025-04-08 [1] CRAN (R 4.5.0)
#>  magrittr       2.0.3      2022-03-30 [1] CRAN (R 4.5.0)
#>  markdown       2.0        2025-03-23 [1] CRAN (R 4.5.0)
#>  pillar         1.11.0     2025-07-04 [1] CRAN (R 4.5.0)
#>  pkgconfig      2.0.3      2019-09-22 [1] CRAN (R 4.5.0)
#>  R6             2.6.1      2025-02-15 [1] CRAN (R 4.5.0)
#>  RColorBrewer   1.1-3      2022-04-03 [1] CRAN (R 4.5.0)
#>  rlang          1.1.6      2025-04-11 [1] CRAN (R 4.5.0)
#>  rmarkdown      2.29       2024-11-04 [1] CRAN (R 4.5.0)
#>  rstudioapi     0.17.1     2024-10-22 [1] CRAN (R 4.5.0)
#>  rversions      2.1.2      2022-08-31 [1] CRAN (R 4.5.0)
#>  scales         1.4.0      2025-04-24 [1] CRAN (R 4.5.0)
#>  sessioninfo    1.2.3      2025-02-05 [1] CRAN (R 4.5.0)
#>  stringi        1.8.7      2025-03-27 [1] CRAN (R 4.5.0)
#>  stringr        1.5.1      2023-11-14 [1] CRAN (R 4.5.0)
#>  svglite        2.2.1      2025-05-12 [1] CRAN (R 4.5.0)
#>  systemfonts    1.2.3      2025-04-30 [1] CRAN (R 4.5.0)
#>  textshaping    1.0.1      2025-05-01 [1] CRAN (R 4.5.0)
#>  tibble         3.3.0      2025-06-08 [1] CRAN (R 4.5.0)
#>  tidyselect     1.2.1      2024-03-11 [1] CRAN (R 4.5.0)
#>  tzdb           0.5.0      2025-03-15 [1] CRAN (R 4.5.0)
#>  utf8           1.2.6      2025-06-08 [1] CRAN (R 4.5.0)
#>  vctrs          0.6.5      2023-12-01 [1] CRAN (R 4.5.0)
#>  viridisLite    0.4.2      2023-05-02 [1] CRAN (R 4.5.0)
#>  vroom          1.6.5      2023-12-05 [1] CRAN (R 4.5.0)
#>  withr          3.0.2      2024-10-28 [1] CRAN (R 4.5.0)
#>  xfun           0.52       2025-04-02 [1] CRAN (R 4.5.0)
#>  xml2           1.3.8      2025-03-14 [1] CRAN (R 4.5.0)
#>  yaml           2.3.10     2024-07-26 [1] CRAN (R 4.5.0)
#> 
#>  [1] /Library/Frameworks/R.framework/Versions/4.5-arm64/library
#>  [2] /Library/Frameworks/R.framework/Versions/4.5-arm64/Resources/library
#>  * ── Packages attached to the search path.
#> 
#> ──────────────────────────────────────────────────────────────────────────────