8  Line Chart: One variable

Objectives

For this chapter I have two objectives for my RWB project:

I will experiment with the basics of building dynamic interactive line charts. This goal includes:

  • Line chart for one country
  • Line chart for several countries with {shiny}
    • Only using native {plotly} functions.
      • Adding countries one by one (see Section 8.2.1).
      • Choosing all countries together and displaying it via actionButton() (see Section 8.2.2).

The final figure with immediate drawing each selected countries includes some simplification and the solution to my colorscale problem (see Section 8.2.3).

8.1 RWB Line Chart / One Country

In the {bslib} documentation there is a basic example of a customizable histogram with numeric variables from the {palmerpenguins} dataset. I will use my own rwb dataset to display different line charts for global scores and rankings for selected countries or regions.

My aim is to go step by step from the simple to the more complex, e.g. to start with a line chart for one country and one variable, followed for several variables and finally with several countries. For my own learning purpose I will also use four different modes:

Table 8.1: Four modes with rising complexity
Program Helper Shiny Abbr.
ggplot2 only —– GO0
plotly only —– PO0
ggplot2 with Shiny GWS
plotly with Shiny PWS

The final product should always be an interactive graph using {plotly} in an web application environment controlled by {shiny} PWS.

In the first try I will not give attention to legend and theme but one: theme_set(theme_bw())

Important 8.1: Conventions

To facilitate learning I will apply in this book three conventions:

  1. To get a better overview of all necessary code lines, I will load the necessary data, functions and packages in every code chunk.
  2. To make it easier for references I will not use Quarto tabsets, because only the content of the visible tab can be cross referenced.
  3. I will develop the shinylive-r code chunks in as separate Shiny app because then I have all available the debug tools. Then I will include the content of the finished app into the shinylive-r code chunk with

## file: app.R
{{< include path-to/app.R >}}

8.1.1 GO0 Line Chart

The easiest line chart is a graph showing the development of one variable over the years for one country. I will take the variable score for the global score showing the trend for my own country Austria (country_en == "Austria).

R Code 8.1 : GO0 Using {ggplot2} (without Shiny) for a line chart

Listing / Output 8.1: One numeric variable (Score) with one factor variable (Country Name) using ggplot(): GO0
Code
library(ggplot2)
library(dplyr, warn.conflicts = FALSE)

rwb <- readRDS(paste0(here::here(), "/data/chap011/rwb/rwb.rds"))

p <- rwb |> 
  select(year_n, country_en, score) |> 
  filter(country_en == "Austria") |> 
  na.omit() |>
  ggplot(aes(year_n, score)) +
    geom_line()

p

Development of the global World Press Freedom Index (WPFI) of Austria with ggplot(): GO0

Development of the global World Press Freedom Index (WPFI) of Austria with ggplot(): GO0

8.1.2 PO0 Line Chart

The UI is identical with R Code 8.4. There are two ways to create a {plotly} line chart:

  1. Convert ggplot() to plotly(). This requires only one changes: Encapsulate the ggplot2::ggplot() object with plotly::ggplotly().
  2. Build a plot_ly() graph from scratch. For me this requires to learn another syntax because my experience with {plotly} is currently very limited. But using this direct approach has some advantages:

R Code 8.2 : PO0: Line chart with ggplot() and ggplotly() (without Shiny).

Listing / Output 8.2: Line chart with ggplot() and ggplotly()
Code
library(plotly, warn.conflicts = FALSE)
library(dplyr, warn.conflicts = FALSE)

rwb <- readRDS(paste0(here::here(), "/data/chap011/rwb/rwb.rds"))

p <- rwb |> 
  select(year_n, country_en, score) |> 
  filter(country_en == "Austria") |> 
  na.omit() |>
  ggplot(aes(year_n, score)) +
    geom_line()

ggplotly(p)

Development of the global World Press Freedom Index (WPFI) of Austria with ggplot() and ggplotly(): GO0

R Code 8.3 : PO0 Using {plotly} (without Shiny) for a line chart

Listing / Output 8.3: One numeric variable (Score) with one factor variable (Country Name) using {plotly}: GO0
Code
library(plotly, warn.conflicts = FALSE)
library(dplyr, warn.conflicts = FALSE)

rwb <- readRDS(paste0(here::here(), "/data/chap011/rwb/rwb.rds"))

p <- rwb |> 
  select(year_n, country_en, score) |> 
  filter(country_en == "Austria") |> 
  na.omit() |>
  plot_ly(
    x = ~year_n, 
    y = ~score, 
    type = 'scatter', 
    mode = 'lines')
p

Development of the global World Press Freedom Index (WPFI) of Austria with {plotly}: PO0

Note 8.1: How to suppress warnings?

During the process of loading and attaching the two packages ({dplyr} and {plotly}) I have used warn.conflicts = FALSE to suppress warnings. This is special for these two packages. A general command would have been base::suppressWarnings().

8.1.3 GWS Line Chart

Now I have to think about the input control(s) for the user. In this first simple example I will only provide to choose one country, specifically only one country. The variable is with score still the same.

Important 8.2: Loading files in Shinylive apps in Quarto

I had problems to work with my RWB dataset. For shinylive-r code chunks is a special procedure necessary to load external data files: There are three ways to include files:

  1. You can embed text files in the code blocks. Not appropriate for me because rwb is a binary file.
  2. You can also embed binary files with a different procedure in the code blocks. But this option didn’t work for me.
  3. Load the file via a http request. This worked for me.

See Shinylive: Load file

How to include .rds file in shinylive (from Brave KI)

To include an .rds file in a Shiny app hosted on the internet, you can load it directly from a public URL using the readRDS function combined with gzcon and url to handle the remote data stream. This method allows you to access the file without needing to download it locally first.

For example, if the .rds file is hosted on a public repository like GitHub, you can use the raw file URL:

data <- readRDS(gzcon(url("https://raw.githubusercontent.com/username/repo/main/data/file.rds")))

This approach works because url() creates a connection to the remote file, gzcon() decompresses it if necessary, and readRDS() reads the R object from the connection This method is particularly useful for deploying Shiny apps on platforms like shinyapps.io, where you can host the data file publicly and reference it directly in your app’s server logic

Alternatively, you can create a helper function to manage the remote loading process, which saves the file to a temporary local location before reading it, ensuring compatibility and reliability:

readRDS_remote <- function(file, quiet = TRUE) {
  if (grepl("^http", file, ignore.case = TRUE)) {
    file_local <- file.path(tempdir(), basename(file))
    download.file(file, file_local, quiet = quiet, mode = "wb")
    file <- file_local
  }
  readRDS(file)
}

Then use it as:

data <- readRDS_remote("https://example.com/data/file.rds")

This method is beneficial when dealing with large files or unreliable connections, as it ensures the file is fully downloaded before being read.

In addition to the line chart I have added two customizations:

  1. I added ggplot2::geom_point() on top of ´ggplot2::geom_line. The result is a connected scatterplot. The page on the R Graph Gallayer show other adaption as well, like custom circles and lines with arguments like shape, size, color and more.
  2. I added a dynamic title for the card container. The somewhat tricky part of this change is that you have to use the paste() function to put the static and dynamic parts of text string together in a reactive function (and not in UI).

8.1.4 PWS Line Chart

8.1.4.1 Using ggploty()

Converting a ggplot() to plotly() in a Shiny app requires three changes:

  1. Change plotOutput() to plotlyOutput() and to
  2. Change renderPlot() to renderPlotly().

R Code 8.5 : PWS Choose country to display WPFI for all available years with ggplotly()

#| '!! shinylive warning !!': |
#|   shinylive does not work in self-contained HTML documents.
#|   Please set `embed-resources: false` in your metadata.
#| standalone: true
#| viewerHeight: 800
#| components: [editor, viewer]
#| layout: vertical

## file: app.R
## app-061-line-chart-pws-1a
## Choose country to display WPFI for all available years
## Using ggplotly()
## @cnj-061-ine-chart-pws-1a

suppressWarnings(suppressPackageStartupMessages({
    library(shiny)
    library(bslib)
    library(ggplot2)
    library(dplyr)
    library(plotly)
}))

rwb <- readRDS(gzcon(url("https://raw.githubusercontent.com/petzi53/rwb-book/master/data/chap011/rwb/rwb.rds")))

theme_set(theme_bw())

ui <- page_sidebar(
    titlePanel("Evolution of the World Press Freedom Index (WPFI) 2013-2025"),
    sidebar = sidebar(
        selectInput(
            inputId = "country",
            label = "Country",
            choices = unique(rwb$country_en)
        )
    ),
    card(
        card_header((textOutput("card_title"))),
        plotlyOutput("p")                              # (1)
    )
)

server <- function(input, output, session) {
    output$card_title <-  renderText({
        paste("World Prees Freedom Index for", input$country)
    })

    output$p <- renderPlotly({                         # (2)
        rwb <- rwb |>
            select(year_n, country_en, score) |>
            filter(country_en == input$country) |>
            na.omit() |>
            ggplot(aes(year_n, score)) +
            geom_line() +
            geom_point()
        ggplotly(rwb)
    })
}

shinyApp(ui, server)

8.1.4.2 Using plot_ly()

The second possibility is to use the native mode to {plotly}: Instead of converting a {ggplot2} graph to {plotly} we generate the interactive graph with plotly::plot_ly().

As this is the more convenient approach to build interactive graphs for complex figures and dashboards computing several charts in parallel, from now on I will only display the native plot_ly() variant.

R Code 8.6 : PWS Choose country to display WPFI for all available years with plot_ly().

#| '!! shinylive warning !!': |
#|   shinylive does not work in self-contained HTML documents.
#|   Please set `embed-resources: false` in your metadata.
#| standalone: true
#| viewerHeight: 800
#| components: [editor, viewer]
#| layout: vertical

## file: app.R
## app-061-line-chart-pws-1b
## Choose country to display WPFI for all available years
## Using native plot_ly()
## @cnj-061-ine-chart-pws-1b

suppressWarnings(suppressPackageStartupMessages({
    library(shiny)
    library(bslib)
    library(dplyr)
    library(plotly)
}))

rwb <- readRDS(gzcon(url("https://raw.githubusercontent.com/petzi53/rwb-book/master/data/chap011/rwb/rwb.rds")))

ui <- page_sidebar(
    titlePanel("Evolution of the World Press Freedom Index (WPFI) 2013-2025"),
    sidebar = sidebar(
        selectInput(
            inputId = "country",
            label = "Country",
            choices = unique(rwb$country_en)
        )
    ),
    card(
        card_header((textOutput("card_title"))),
        plotlyOutput("p")
    )
)

server <- function(input, output, session) {
    output$card_title <-  renderText({
        paste("World Prees Freedom Index for", input$country)
    })

    output$p <- renderPlotly({
        rwb |>
            select(year_n, country_en, score) |>
            filter(country_en == input$country) |>
            na.omit() |>
            plot_ly(
                x = ~year_n,
                y = ~score,
                type = 'scatter',
                mode = 'lines+markers')
    })
}

shinyApp(ui, server)

Important 8.2: Some comments on the plot_ly() syntax
  1. Note that the x and y variable need in front the ~ sign. These are the data visualized as scatter point or lines in the x and y variable.
  2. Type scatter is a fundamental type for creating various visualizations such as scatter plots, line charts, but is also used for text and bubble charts.
  3. The mode attribute determines how the data is displayed, such as with markers, lines, text, or a combination of these. For example, setting mode = "line" creates a standard line plot as in Listing / Output 8.3, while mode = "lines+markers" adds both lines connecting the points and markers at each point as in R Code 8.6.

Resource 8.1 : How to build line and scatter plots with {plotly}

8.2 RWB Line Charts / Several Countries

8.2.1 One country after the other

R Code 8.7 : Show development of WPFI for several countries at once

#| '!! shinylive warning !!': |
#|   shinylive does not work in self-contained HTML documents.
#|   Please set `embed-resources: false` in your metadata.
#| standalone: true
#| viewerHeight: 800
#| components: [editor, viewer]
#| layout: vertical

## file: app.R
## app-061-line-chart-pws-2
## Choose several countries to display WPFI for all available years
## @cnj-061-ine-chart-pws-2

suppressWarnings(suppressPackageStartupMessages({
    library(shiny)
    library(bslib)
    library(dplyr)
    library(plotly)
}))


rwb <- readRDS(gzcon(url("https://raw.githubusercontent.com/petzi53/rwb-book/master/data/chap011/rwb/rwb.rds")))

ui <- page_sidebar(
    titlePanel("Evolution of the World Press Freedom Index (WPFI) 2013-2025"),
    sidebar = sidebar(
        selectInput(
            inputId = "country",
            label = "Choose countries",
            choices = unique(rwb$country_en),
            multiple = TRUE
        )
    ),
    card(
        card_header((textOutput("card_title"))),
        plotlyOutput("p")
    )
)

server <- function(input, output, session) {

    output$card_title <-  renderText({
        my_countries <- filter(countries(), country_en %in% input$country)
        txt <- unique(my_countries$country_en)
        s = paste("World Press Freedom Index:", txt[1])
        if (length(txt) > 1) {
            for (i in 2:length(txt)) {
                s <- paste(s, txt[i], sep = ", ")
            }
        }
        s
    })

    countries <- reactive({
        req(input$country)
        rwb |>
            select(year_n, score, country_en) |>
            filter(country_en %in% input$country) |>
            arrange(year_n) |>
            na.omit() |>
            droplevels()
    })

    output$p <- renderPlotly({
        req(countries())
        plotly::plot_ly(
            data = countries(),
            x = ~year_n,
            y = ~score,
            color = ~country_en,
            colors = RColorBrewer::brewer.pal(12, "Paired"),
            type = 'scatter',
            mode = 'lines+markers',
            marker = list(size = 10)
        )
    })
}

shinyApp(ui, server)

There are several important comments to make:

  1. At first I tried to disntinguish between the first trace (with plot_ly()) and all the other traces with add_trace(). But it turned out that I just need to set the argument color to the country vector.

  2. I had to adapt the card title so that it can display all names of the displayed countries.

  3. A big drawback is that the line color of the already chosen countries changes after another country is selected. So far I couldn’t find a solution. After my question was in StackOverflow not accepted (supposedly because it is a duplicate of another question), I posted in the Posit Forum for help.

8.2.2 With actionButton()

A different UI would be adding an action button to delay the reaction. In this case the complete chart with all the chosen countries is drawn. So there is no irritation.

But in the next step when adding or removing the same problem recurs. Adding just one country results again in a distracting experience. It is a slightly better whenever several countries are added, removed or changed.

R Code 8.8 : Show development of WPFI with delayed reaction via action button

#| '!! shinylive warning !!': |
#|   shinylive does not work in self-contained HTML documents.
#|   Please set `embed-resources: false` in your metadata.
#| standalone: true
#| viewerHeight: 800
#| components: [editor, viewer]
#| layout: vertical

## file: app.R
## app-061-line-chart-pws-3
## Choose several countries to display WPFI for all available years
## @cnj-061-ine-chart-pws-3


suppressWarnings(suppressPackageStartupMessages({
    library(shiny)
    library(bslib)
    library(dplyr)
    library(plotly)
}))

rwb <- readRDS(gzcon(url("https://raw.githubusercontent.com/petzi53/rwb-book/master/data/chap011/rwb/rwb.rds")))

ui <- page_sidebar(
    titlePanel("Evolution of the World Press Freedom Index (WPFI) 2013-2025"),
    sidebar = sidebar(
        selectInput(
            inputId = "country",
            label = "Country",
            choices = unique(rwb$country_en),
            multiple = TRUE
        ),
        actionButton("go", "Go!", class = "btn-success")
    ),
    card(
        card_header((textOutput("card_title"))),
        plotlyOutput("my_chart")
    )
)

server <- function(input, output, session) {

    countries <- eventReactive(input$go, {
        output$card_title <-  renderText({
            s = paste("World Press Freedom Index for", input$country[1])
            if (length(input$country)  > 1) {
                for (i in 2:length(input$country)) {
                    s <- paste(s, input$country[i], sep = ", ")
                }
            }
            s
        })
        rwb |>
            select(year_n, score, country_en) |>
            filter(country_en %in% input$country) |>
            arrange(year_n) |>
            na.omit() |>
            droplevels()
    })

    output$my_chart <- renderPlotly({
        plotly::plot_ly(
            data = countries(),
            x = ~year_n,
            y = ~score,
            color = ~country_en,
            colors = RColorBrewer::brewer.pal(12, "Paired"),
            type = 'scatter',
            mode = 'lines+markers',
            marker = list(size = 10)
        )
    })
}

shinyApp(ui, server)



8.2.3 Solution fixed color

I finally found the solution to my problem of changing the line color of the already chosen countries. The part I didn’t understand was that I needed a named color vector as demonstrated in the second example of Custom Color Scales of the Plotly website.

pal <- c("red", "blue", "green")
pal <- setNames(pal, c("virginica", "setosa", "versicolor"))

I didn’t apply the second part with setNames() to the color palette pal. A loop — as I thought — is not necessary.

I have commented the three added lines and also the one changed line. Additionally I have simplified the card_title() rendering function and moved under the final renderPlotly() function.

R Code 8.9 : Show development of WPFI with consistent colored country lines

#| '!! shinylive warning !!': |
#|   shinylive does not work in self-contained HTML documents.
#|   Please set `embed-resources: false` in your metadata.
#| standalone: true
#| viewerHeight: 800
#| components: [editor, viewer]
#| layout: vertical

## file: app.R
## app-061-line-chart-pws-solution
## Choose several countries to display WPFI for all available years
## @cnj-061-ine-chart-pws-solution

suppressWarnings(suppressPackageStartupMessages({
    library(shiny)
    library(bslib)
    library(dplyr)
    library(plotly)
}))


rwb <- readRDS(gzcon(url("https://raw.githubusercontent.com/petzi53/rwb-book/master/data/chap011/rwb/rwb.rds")))

ui <- page_sidebar(
    titlePanel("Evolution of the World Press Freedom Index (WPFI) 2013-2025"),
    sidebar = sidebar(
        selectInput(
            inputId = "country",
            label = "Choose countries",
            choices = unique(rwb$country_en),
            multiple = TRUE
        )
    ),
    card(
        card_header((textOutput("card_title"))),
        plotlyOutput("p")
    )
)

server <- function(input, output, session) {

    pal = RColorBrewer::brewer.pal(12, "Paired")                # added

    countries <- reactive({
        req(input$country)

        # transferred and simplified #######################
        output$card_title <-  renderText({
            s = paste(
                "World Press Freedom Index for",
                input$country[1]
                )
            if (length(input$country)  > 1) {
                for (i in 2:length(input$country)) {
                    s <- paste(s, input$country[i], sep = ", ")
                }
            }
            s
        })
        #####################################################

        rwb |>
            select(year_n, score, country_en) |>
            filter(country_en %in% input$country) |>
            arrange(year_n) |>
            na.omit() |>
            droplevels()
    })

    output$p <- renderPlotly({
        req(countries())
        length(pal) <- length(input$country)                    # added
        pal <- setNames(pal, input$country)                     # added
        plotly::plot_ly(
            data = countries(),
            x = ~year_n,
            y = ~score,
            color = ~country_en,
            colors = pal,                                       # changed
            type = 'scatter',
            mode = 'lines+markers',
            marker = list(size = 10)
        )
    })
}

shinyApp(ui, server)



Glossary Entries

term definition
RWB Reporters Without Borders (RWB), known by its French name Reporters sans frontières and acronym RSF, is an international non-profit and non-governmental organization headquartered in Paris, France, founded in 1985 in Montpellier by journalists Robert Ménard, Rémy Loury, Jacques Molénat, and Émilien Jubineau. It is dedicated to safeguarding the right to freedom of information and defends journalists and media personnel who are imprisoned, persecuted, or at risk for their work. The organization has consultative status at the United Nations, UNESCO, the Council of Europe, and the International Organisation of the Francophonie.

Session Info

Session Info

Code
xfun::session_info()
#> R version 4.5.1 (2025-06-13)
#> Platform: aarch64-apple-darwin20
#> Running under: macOS Sequoia 15.6.1
#> 
#> Locale: en_US.UTF-8 / en_US.UTF-8 / en_US.UTF-8 / C / en_US.UTF-8 / en_US.UTF-8
#> 
#> Package version:
#>   askpass_1.2.1       base64enc_0.1.3     bslib_0.9.0        
#>   cachem_1.1.0        cli_3.6.5           commonmark_2.0.0   
#>   compiler_4.5.1      cpp11_0.5.2         crosstalk_1.2.2    
#>   curl_7.0.0          data.table_1.17.8   dichromat_2.0-0.1  
#>   digest_0.6.37       dplyr_1.1.4         evaluate_1.0.5     
#>   farver_2.1.2        fastmap_1.2.0       fontawesome_0.5.3  
#>   fs_1.6.6            generics_0.1.4      ggplot2_3.5.2      
#>   glossary_1.0.0.9003 glue_1.8.0          graphics_4.5.1     
#>   grDevices_4.5.1     grid_4.5.1          gtable_0.3.6       
#>   here_1.0.1          highr_0.11          htmltools_0.5.8.1  
#>   htmlwidgets_1.6.4   httr_1.4.7          isoband_0.2.7      
#>   jquerylib_0.1.4     jsonlite_2.0.0      kableExtra_1.4.0   
#>   knitr_1.50          labeling_0.4.3      later_1.4.4        
#>   lattice_0.22.7      lazyeval_0.2.2      lifecycle_1.0.4    
#>   litedown_0.7        magrittr_2.0.3      markdown_2.0       
#>   MASS_7.3.65         Matrix_1.7.4        memoise_2.0.1      
#>   methods_4.5.1       mgcv_1.9.3          mime_0.13          
#>   nlme_3.1.168        openssl_2.3.3       pillar_1.11.0      
#>   pkgconfig_2.0.3     plotly_4.11.0       promises_1.3.3     
#>   purrr_1.1.0         R6_2.6.1            rappdirs_0.3.3     
#>   RColorBrewer_1.1-3  Rcpp_1.1.0          rlang_1.1.6        
#>   rmarkdown_2.29      rprojroot_2.1.1     rstudioapi_0.17.1  
#>   rversions_2.1.2     rvest_1.0.5         sass_0.4.10        
#>   scales_1.4.0        selectr_0.4.2       splines_4.5.0      
#>   stats_4.5.1         stringi_1.8.7       stringr_1.5.1      
#>   svglite_2.2.1       sys_3.4.3           systemfonts_1.2.3  
#>   textshaping_1.0.1   tibble_3.3.0        tidyr_1.3.1        
#>   tidyselect_1.2.1    tinytex_0.57        tools_4.5.1        
#>   utf8_1.2.6          utils_4.5.1         vctrs_0.6.5        
#>   viridisLite_0.4.2   withr_3.0.2         xfun_0.53          
#>   xml2_1.4.0          yaml_2.3.10

References

Sievert, Carson. 2019. Interactive Web-Based Data Visualization with r, Plotly, and Shiny. https://plotly-r.com/.