43  Информационные панели с Shiny

Информационные панели часто являются замечательным способом предоставления результатов анализа другим людям. Создание информационных панелей с shiny требует относительно продвинутых знаний языка R, но дает замечательный уровень индивидуализации и возможностей.

Человеку, изучающему информационные панели с shiny, рекомендуется иметь хорошие знания преобразования и визуализации данных, уметь проводить дебаггинг кода и писать функции. Работа с информационными панелями в самом начале не кажется интуитивно понятной, иногда их сложно понять, но это отличный навык и с практикой все становится проще!

На этой странице будет сделан краткий обзор того, как создавать информационные панели с shiny и его расширениями. Для альтернативного метода создания информационных панелей, который быстрее, проще, но менее индивидуализированный, см. страницу по flextable (Информационные панели с R Markdown).

43.1 Подготовка

Загрузка пакетов

В данном руководстве мы подчеркиваем использование p_load() из pacman, который устанавливает пакет, если необходимо, и загружает его для использования. Вы можете также загрузить установленные пакеты с помощью library() из базового R. См. страницу Основы R для получения дополнительной информации о пакетах R.

Начнем с установки пакета R shiny:

pacman::p_load("shiny")

Импорт данных

Если вы хотите выполнять действия параллельно на этой странице, см. этот раздел в Скачивание руководства и данных. Есть ссылки для скачивания скриптов R и файлов с данными, которые создают итоговое приложение Shiny.

Если вы попытаетесь восстановить приложение, используя эти файлы, обратите внимание на структуру папок проекта R, которая создается в ходе демонстрации (например, папки “data” и “funcs”).

43.2 Структура приложения shiny

Базовая структура файлов

Чтобы разобраться в shiny, нам сначала нужно понять, как работает структура файлов приложения! Мы должны создать совершенно новую директорию перед началом. Это можно упростить, выбрав New project (новый проект) в Rstudio, затем выбрав Shiny Web Application (веб-приложение Shiny). Это позволит создать базовую структуру приложения shiny для вас.

при открытии этого проекта вы заметите, что уже присутствует файл .R под названием app.R. Очень важно, чтобы у нас была одна из двух основных структур файла:

  1. Один файл под названием app.R, или
  2. Два файла, один под названием ui.R и другой под названием server.R

На этой странице мы используем сначала первый подход с наличием одного файла под названием app.R. Вот пример скрипта:

# пример app.R

library(shiny)

ui <- fluidPage(

    # Название приложения
    titlePanel("My app"),

    # Боковая панель с виджетом ввода данных
    sidebarLayout(
        sidebarPanel(
            sliderInput("input_1")
        ),

        # Показать график 
        mainPanel(
           plotOutput("my_plot")
        )
    )
)

# Задаем логику сервера, требуемую для рисования гистограммы
server <- function(input, output) {
     
     plot_1 <- reactive({
          plot_func(param = input_1)
     })
     
    output$my_plot <- renderPlot({
       plot_1()
    })
}


# Выполняем приложение 
shinyApp(ui = ui, server = server)

Если вы откроете этот файл, вы заметите, что определено два объекта - один под названием ui, а другой - server. Эти объекты должны быть определены в каждом приложении shiny и являются центральными для структуры самого приложения! На самом деле, единственной разницей между двумя структурами файлов, описанных выше, является то, в структуре 1, и ui и server задаются в одном файле, а в структуре 2 они определяются в разных файлах. Примечание: мы можем также (и нам следует, если у нас более крупное приложение) иметь другие файлы .R в нашепй структуре, которые мы можем вызывать с помощью source() в приложении.

Server (сервер) и ui (пользовательский интерфейс)

Нам нужно понять, что делают объекты server и ui. Если говорить простыми словами, эти два объекта взаимодействуют друг с другом, когда пользователь взаимодействует с приложением shiny.

Элемент UI приложения shiny, по сути, является кодом R, который создает HTML интерфейс. Это значит все, что отображается в пользовательском интерфейсе (UI) приложения. Это, как правило, включает:

  • “Виджеты” - выпадающие меню, поля для галочек, окна прокрутки и т.п., с которыми может взаимодействовать пользователь
  • Графики, таблицы и т.п. - выходные данные, которые генерирует код R
  • Навигационные аспекты приложения - вкладки, панели и т.п.
  • Общий текст, гиперссылки и т.п.
  • HTML и CSS элементы (будут рассмотрены позже)

Наиболее важной вещью, которую нужно понять про UI, является то, что он получает ввод от пользователя и отображает выходные данные от сервера. В ui никогда не выполняется активный код - все изменения, которые мы видим в UI, передаются через сервер (плюс минус). Поэтому мы создаем графики, проводим скачивание и т.п. на сервере.

На сервере приложения shiny выполняется весь код после запуска приложения. То, как это работает, может немного запутать. Функция сервера, по сути, реагирует на взаимодействие пользователя с UI, и в качестве отклика выполняет фрагменты кода. Если что-то меняется на сервере, это передается на ui, где можно увидеть изменения. Что важно, код на сервере исполняется не-последовательно (или по крайней мере лучше так думать). по сути, когда ввод через ui влияет на фрагмент кода на сервере, он будет выполнен автоматически, выходные данные будут подготовлены и отображены.

Сейчас это все звучит очень абстрактно, но мы углубимся в некоторые примеры, чтобы получить представление о том, как это работает.

Прежде чем вы начнете строить приложение

Прежде чем вы начнете строить приложение, очень полезно знать - что вы хотите построить. Поскольку ваш пользовательский интерфейс будет написан в коде, вы не сможете представить себе, что именно вы создаете, если только не стремитесь к чему-то конкретному. По этой причине очень полезно посмотреть множество примеров блестящих приложений, чтобы получить представление о том, что можно создать, а еще лучше, если вы сможете заглянуть в исходный код этих приложений! Отличными ресурсами для этого являются:

Как только вы получите представление о том, что возможно, полезно также наметить, как вы хотите, чтобы выглядело ваше приложение, - это можно сделать на бумаге или в любой программе для рисования (PowerPoint, MS paint и т.д.). Для первого приложения полезно начать с простого! Нет ничего постыдного в том, чтобы использовать найденный в Интернете код хорошего приложения в качестве шаблона для своей работы - это гораздо проще, чем создавать что-то с нуля!

43.3 Построение пользовательского интерфейса (UI)

При создании приложения проще сначала поработать над пользовательским интерфейсом, чтобы видеть, что мы делаем, и не рисковать тем, что приложение не будет работать из-за ошибок сервера. Как уже говорилось, при работе над пользовательским интерфейсом часто полезно использовать шаблон. Существует ряд стандартных макетов, которые можно использовать в shiny, доступных из базового пакета shiny, но стоит отметить, что есть и ряд расширений пакета, таких как shinydashboard. Мы используем для начала пример базового shiny.

Пользовательский интерфейс UI, как правило, определяется как серия вложенных функций в следующем порядке:

  1. Функция, определяющая общий макет (самая простая - fluidPage(), но есть и другие)
  2. Панели внутри макета, такие как:
    • боковая панель (sidebarPanel())
    • “основная” панель (mainPanel())
    • вкладка (tabPanel())
    • общий “столбец” (column())
  3. Виджеты и выходные данные - они могут предоставлять входные данные на сервер (виджеты) или выходные данные с сервера (выходы)
    • Виджеты, как правило, стилизуются как xxxInput() например, selectInput()
    • Выходные данные, как правило, стилизуются как xxxOutput() например, plotOutput()

Стоит еще раз подчеркнуть, что эти данные не могут быть легко визуализированы в абстрактном виде, поэтому лучше обратиться к примеру! Давайте рассмотрим возможность создания базового приложения, визуализирующего данные о количестве малярийных учреждений по районам. Эти данные содержат множество различных параметров, поэтому было бы здорово, если бы конечный пользователь мог применить некоторые фильтры, чтобы увидеть данные по возрастным группам/районам так, как он считает нужным! Для начала мы можем использовать очень простой блестящий макет - макет боковой панели. Это макет, в котором виджеты располагаются в боковой панели слева, а график - справа.

Давайте спланируем наше приложение - мы можем начать с селектора, который позволяет нам выбрать, для какого района мы хотим визуализировать данные, а еще один позволит визуализировать возрастную группу, которая нам интересна. Мы будем использовать эти фильтры, чтобы показать эпидкривую, которая отображает эти параметры. Для этого нам нужно:

  1. Два выпадающих меню, которые позволяют нам выбрать, какой район и возрастная группа нам нужны.
  2. Область, где мы можем показать полученную в результате эпидкривую.

Это может выглядеть следующим образом:

library(shiny)

ui <- fluidPage(

  titlePanel("Malaria facility visualisation app"),

  sidebarLayout(

    sidebarPanel(
         # селектор района
         selectInput(
              inputId = "select_district",
              label = "Select district",
              choices = c(
                   "All",
                   "Spring",
                   "Bolo",
                   "Dingo",
                   "Barnard"
              ),
              selected = "All",
              multiple = TRUE
         ),
         # селектор возрастной группы
         selectInput(
              inputId = "select_agegroup",
              label = "Select age group",
              choices = c(
                   "All ages" = "malaria_tot",
                   "0-4 yrs" = "malaria_rdt_0-4",
                   "5-14 yrs" = "malaria_rdt_5-14",
                   "15+ yrs" = "malaria_rdt_15"
              ), 
              selected = "All",
              multiple = FALSE
         )

    ),

    mainPanel(
      # эпидкривая здесь
      plotOutput("malaria_epicurve")
    )
    
  )
)

Когда выполняется app.R с указанным выше кодом UI (без активного кода в серверной части (server) app.R) макет выглядит вот так - обратите внимание, что не будет графика, если нет сервера, который его сформирует, но наши вводы работают!

Это отличная возможность обсудить то, как работают виджеты - обратите внимание, что каждый виджет принимает inputId, label и ряд других опций в зависимости от типа виджета. Этот inputId очень важен - есть два идентификационных номера (ID), которые используются для передачи информации от пользовательского интерфейса на сервер. Поэтому они должны быть уникальны. Вы должны постараться назвать их понятно и специфично для того взаимодействия, которое они осуществляют, особенно в больших приложениях.

Вы должны внимательно прочитать подробные детали о работе каждого виджета в документации. Виджете передают конкретные типы данных на сервер в зависимости от типа виджета, это необходимо хорошо понимать. Например, selectInput() передает буквенный тип на сервер:

  • Если мы выберем Spring для этого виджета, он передаст объект "Spring" на сервер.
  • Если мы выберем два пункта из выпадающего меню, они будут переданы как текстовый вектор (например, c("Spring", "Bolo")).

Другие виджеты передают другие типы объектов на сервер! Например:

  • numericInput() передаст объект числового типа на сервер
  • checkboxInput() передаст объект логического типа на сервер (TRUE или FALSE)

Также следует отметить именованный вектор, который мы использовали для данных о возрасте. Для многих виджетов используется именованный вектор, так как варианты будут отображаться как имена вектора в виде выводих вариантов, но они передают выбранное значение из вектора серверу. Т.е., человек может выбрать “15+” из выпадающего меню и UI передаст на сервер "malaria_rdt_15" - что является именем столбца, который нам нужен!

Существует множество виджетов, с помощью которых можно выполнять различные действия в приложении. Виджеты также позволяют загружать файлы в приложение и выгружать результаты. Есть также несколько отличных расширений shiny, которые дают доступ к большему количеству виджетов, чем базовый shiny - пакет shinyWidgets является замечательным примером этого. Чтобы изучить некоторые примеры, см. следующие ссылки:

43.4 Загрузка данных в приложение

Следующим шагом в разработке нашего приложения является запуск сервера. Для этого нам необходимо получить некоторые данные в нашем приложении и определить все вычисления, которые мы собираемся выполнять. Блестящее приложение не так просто отладить, поскольку часто неясно, откуда берутся ошибки, поэтому идеальным вариантом будет заставить работать весь наш код обработки данных и визуализации до того, как мы начнем создавать сам сервер.

Итак, если мы хотим сделать приложение, которое будет показывать эпидкривые, изменяющиеся в зависимости от ввода пользователя, то нам следует подумать о том, какой код нам потребуется для запуска этого приложения в обычном скрипте на R. Нам потребуется:

  1. загрузить пакеты
  2. загрузить данные
  3. преобразовать данные
  4. разработать функцию для визуализации данных в зависимости от ввода пользователя

Этот перечень достаточно простой и не должен вызывать сложностей при реализации. Теперь важно подумать, какие части процесса должны быть выполнены только один раз, а какие части нужно выполнять в ответ за пользовательский ввод. Это происходит потому, что приложения shiny, как правило, выполняют некоторый код до запуска, что делается только один раз. Для быстроты работы нашего приложения будет полезно перенести как можно больше кода в этот раздел. Например, нам нужно загрузить данные/пакеты и выполнить основные преобразования только один раз, Поэтому мы можем разместить этот код за пределами сервера. Это означает, что единственное, что будет делать сервер - выполнять код для визуализации данных. Давайте разработаем все эти компоненты сначала в скрипте. Однако поскольку мы визуализируем данные с помощью функции, мы также можем разместить код для функции за пределами сервера, чтобы наша функция была в среде, когда приложение запускается!

Сначала загрузим наши данные. Поскольку мы работаем с новым проектом и хотим сделать его чистым, мы можем создать новую директорию под названием data и добавить туда наши данные о малярии. Мы можем запустить приведенный ниже код в тестовом скрипте, который впоследствии удалим, когда очистим структуру нашего приложения.

pacman::p_load("tidyverse", "lubridate")

# прочитываем данные
malaria_data <- rio::import(here::here("data", "malaria_facility_count_data.rds")) %>% 
  as_tibble()

print(malaria_data)
# A tibble: 3,038 × 10
   location_name data_date  submitted_date Province District `malaria_rdt_0-4`
   <chr>         <date>     <date>         <chr>    <chr>                <int>
 1 Facility 1    2020-08-11 2020-08-12     North    Spring                  11
 2 Facility 2    2020-08-11 2020-08-12     North    Bolo                    11
 3 Facility 3    2020-08-11 2020-08-12     North    Dingo                    8
 4 Facility 4    2020-08-11 2020-08-12     North    Bolo                    16
 5 Facility 5    2020-08-11 2020-08-12     North    Bolo                     9
 6 Facility 6    2020-08-11 2020-08-12     North    Dingo                    3
 7 Facility 6    2020-08-10 2020-08-12     North    Dingo                    4
 8 Facility 5    2020-08-10 2020-08-12     North    Bolo                    15
 9 Facility 5    2020-08-09 2020-08-12     North    Bolo                    11
10 Facility 5    2020-08-08 2020-08-12     North    Bolo                    19
# ℹ 3,028 more rows
# ℹ 4 more variables: `malaria_rdt_5-14` <int>, malaria_rdt_15 <int>,
#   malaria_tot <int>, newid <int>

Работать с этими данными будет проще, если мы будем использовать аккуратные стандарты данных, поэтому нам также следует преобразовать их в более длинный формат, где возрастная группа - это столбец, а случаи - другой столбец. Мы это легко можем сделать с помощью принципов, которые были рассмотрены на странице [Поворот данных].

malaria_data <- malaria_data %>%
  select(-newid) %>%
  pivot_longer(cols = starts_with("malaria_"), names_to = "age_group", values_to = "cases_reported")

print(malaria_data)
# A tibble: 12,152 × 7
   location_name data_date  submitted_date Province District age_group       
   <chr>         <date>     <date>         <chr>    <chr>    <chr>           
 1 Facility 1    2020-08-11 2020-08-12     North    Spring   malaria_rdt_0-4 
 2 Facility 1    2020-08-11 2020-08-12     North    Spring   malaria_rdt_5-14
 3 Facility 1    2020-08-11 2020-08-12     North    Spring   malaria_rdt_15  
 4 Facility 1    2020-08-11 2020-08-12     North    Spring   malaria_tot     
 5 Facility 2    2020-08-11 2020-08-12     North    Bolo     malaria_rdt_0-4 
 6 Facility 2    2020-08-11 2020-08-12     North    Bolo     malaria_rdt_5-14
 7 Facility 2    2020-08-11 2020-08-12     North    Bolo     malaria_rdt_15  
 8 Facility 2    2020-08-11 2020-08-12     North    Bolo     malaria_tot     
 9 Facility 3    2020-08-11 2020-08-12     North    Dingo    malaria_rdt_0-4 
10 Facility 3    2020-08-11 2020-08-12     North    Dingo    malaria_rdt_5-14
# ℹ 12,142 more rows
# ℹ 1 more variable: cases_reported <int>

На этом мы закончили подготовку данных! Это вычеркивает пункты 1, 2 и 3 из списка того, что необходимо разработать для “тестового R-скрипта”. Последней и самой сложной задачей будет построение функции для получения эпидкривой по заданным пользователем параметрам. Как уже говорилось, тем кто изучает shiny сначала настоятельно рекомендуется изучить раздел по функциональному программированию ([Написание функций]), чтобы понять, как это работает!

При определении функции может возникнуть трудность с выбором параметров, которые мы хотим включить в нее. При функциональном программировании с помощью shiny каждый релевантный параметр, как правило, имеет виджет, связанный с ним, поэтому думать об этом обычно довольно просто! Например, в нашем текущем приложении мы хотим иметь возможность фильтровать по районам, и для этого у нас есть виджет, поэтому мы можем добавить параметр district, чтобы отразить это. У нас нет функционала в приложении для фильтра по медицинским организациям (пока), поэтому нам нет необходимости добавлять их как параметр. Давайте начнем с создания функции с тремя параметрами:

  1. Ключевой набор данных
  2. Выбранный район
  3. Выбранная возрастная группа
plot_epicurve <- function(data, district = "All", agegroup = "malaria_tot") {
  
  if (!("All" %in% district)) {
    data <- data %>%
      filter(District %in% district)
    
    plot_title_district <- stringr::str_glue("{paste0(district, collapse = ', ')} districts")
    
  } else {
    
    plot_title_district <- "all districts"
    
  }
  
  # если нет оставшихся данных, выдать NULL
  if (nrow(data) == 0) {
    
    return(NULL)
  }
  
  data <- data %>%
    filter(age_group == agegroup)
  
  
  # если нет оставшихся данных, выдать NULL
  if (nrow(data) == 0) {
    
    return(NULL)
  }
  
  if (agegroup == "malaria_tot") {
      agegroup_title <- "All ages"
  } else {
    agegroup_title <- stringr::str_glue("{str_remove(agegroup, 'malaria_rdt')} years")
  }
  
  
  ggplot(data, aes(x = data_date, y = cases_reported)) +
    geom_col(width = 1, fill = "darkred") +
    theme_minimal() +
    labs(
      x = "date",
      y = "number of cases",
      title = stringr::str_glue("Malaria cases - {plot_title_district}"),
      subtitle = agegroup_title
    )
  
  
  
}

Мы не будем подробно останавливаться на этой функции, поскольку ее работа достаточно проста. Однако следует отметить, что при обработке ошибок мы выдаем NULL, когда в противном случае была бы выдана ошибка. Это связано с тем, что если сервер shiny создает объект NULL вместо объекта графика, ничего не отобразится в пользовательском интерфейсе! Это очень важно, поскольку в противном случае ошибки часто приводят к остановке работы приложения.

Следует также отметить использование оператора %in% при оценке ввода района district. Как упомянуто выше, он может быть задан как текстовый вектор с несколькими значениями, поэтому использование %in% даст большую гибкость, чем, например, ==.

Давайте протестируем нашу функцию!

plot_epicurve(malaria_data, district = "Bolo", agegroup = "malaria_rdt_0-4")

Если наша функция работает, нам надо понять, как это все будет встроено в наше приложение shiny. Мы упоминали концепцию кода предзапуска ранее, но давайте рассмотрим, как его включить в структуру нашего приложения. Это можно сделать двумя способами!

  1. разместить код в файле app.R в начале скрипта (выше UI), или
  2. создать новый файл в директории приложения под названием global.R, и разместить код предзапуска в этом файле.

Стоит отметить, что обычно, особенно в больших приложениях, проще использовать вторую файловую структуру, так как она позволяет разделить файловую структуру простым способом. Давайте теперь полностью разработаем этот скрипт global.R. Вот как он может выглядеть:

# скрипт global.R

pacman::p_load("tidyverse", "lubridate", "shiny")

# прочитываем данные
malaria_data <- rio::import(here::here("data", "malaria_facility_count_data.rds")) %>% 
  as_tibble()

# вычищаем данные и поворачиваем вертикально
malaria_data <- malaria_data %>%
  select(-newid) %>%
  pivot_longer(cols = starts_with("malaria_"), names_to = "age_group", values_to = "cases_reported")


# определяем построение функций
plot_epicurve <- function(data, district = "All", agegroup = "malaria_tot") {
  
  # создаем заголовок графика
  if (!("All" %in% district)) {            
    data <- data %>%
      filter(District %in% district)
    
    plot_title_district <- stringr::str_glue("{paste0(district, collapse = ', ')} districts")
    
  } else {
    
    plot_title_district <- "all districts"
    
  }
  
  # если нет оставшихся данных, выдать NULL
  if (nrow(data) == 0) {
    
    return(NULL)
  }
  
  # фильтр по возрастной группе
  data <- data %>%
    filter(age_group == agegroup)
  
  
  # если нет оставшихся данных, выдать NULL
  if (nrow(data) == 0) {
    
    return(NULL)
  }
  
  if (agegroup == "malaria_tot") {
      agegroup_title <- "All ages"
  } else {
    agegroup_title <- stringr::str_glue("{str_remove(agegroup, 'malaria_rdt')} years")
  }
  
  
  ggplot(data, aes(x = data_date, y = cases_reported)) +
    geom_col(width = 1, fill = "darkred") +
    theme_minimal() +
    labs(
      x = "date",
      y = "number of cases",
      title = stringr::str_glue("Malaria cases - {plot_title_district}"),
      subtitle = agegroup_title
    )
  
  
  
}

Все легко! Замечательное свойство shiny в том, что оно может понять, для чего нужны файлы с именами app.R, server.R, ui.R, и global.R, поэтому нет необходимости каждый раз их связывать между собой с помощью кода. Поэтому просто если у нас есть этот код в global.R в директории, он будет выполнен до запуска приложения!.

Следует также отметить, что для улучшения организации нашего приложения было бы полезно перенести функцию построения в отдельный файл - это будет особенно полезно при увеличении размера приложений. Чтобы это сделать, мы можем создать еще одну директорию под названием funcs и разместить эту функцию как файл под названием plot_epicurve.R. Мы затем можем прочитать эту функцию с помощью следующей команды в global.R

source(here("funcs", "plot_epicurve.R"), local = TRUE)

Обратите внимание, что нужно всегда уточнять local = TRUE в приложениях shiny, поскольку это влияет на вызов, если приложение будет опубликовано на сервере.

43.5 Разработка сервера приложения

Теперь, когда у нас есть большая часть кода, нам осталось разработать наш сервер. Это последняя часть нашего приложения, и, вероятно, самая сложная для понимания. Сервер представляет собой большую функцию R, но полезно представить его как серию более мелких функций или задач, которые может выполнять приложение. Важно понимать, что эти функции не выполняются в линейном порядке. Они выполняются в определенном порядке, но это не обязательно понимать в начале работы с shiny. На самом базовом уровне эти задачи или функции активизируются при изменении пользовательского ввода, который влияет на них, кроме случаев, когда разработчик задал для них другое поведение. Опять же, это все весьма абстрактно, но сначала давайте разберем три базовых типа объектов shiny.

  1. Реагирующие источники - это еще один термин для пользовательских вводов. Сервер shiny имеет доступ к выходным данным из UI через виджеты, которые мы запрограммировали. Каждый раз, когда меняются значения в них, это передается на сервер.

  2. Реагирующие проводники - это объекты, которые существуют только внутри сервера shiny. Нам они не нужны для простых приложений, но они создают объекты, которые могут быть видны только внутри сервера и используются в других операциях. Они, как правило, зависят от реагирующих источниках.

  3. Конечные точки - это выходные данные, которые передаются от сервера на пользовательский интерфейс (UI). В нашем примере это будет эпидкривая, которую мы создаем.

Помня об этом давайте построим наш сервер шаг за шагом. Мы покажем снова наш код UI для справки:

ui <- fluidPage(

  titlePanel("Malaria facility visualisation app"),

  sidebarLayout(

    sidebarPanel(
         # селектор района
         selectInput(
              inputId = "select_district",
              label = "Select district",
              choices = c(
                   "All",
                   "Spring",
                   "Bolo",
                   "Dingo",
                   "Barnard"
              ),
              selected = "All",
              multiple = TRUE
         ),
         # селектор возрастной группы
         selectInput(
              inputId = "select_agegroup",
              label = "Select age group",
              choices = c(
                   "All ages" = "malaria_tot",
                   "0-4 yrs" = "malaria_rdt_0-4",
                   "5-14 yrs" = "malaria_rdt_5-14",
                   "15+ yrs" = "malaria_rdt_15"
              ), 
              selected = "All",
              multiple = FALSE
         )

    ),

    mainPanel(
      # эпидкривая сюда
      plotOutput("malaria_epicurve")
    )
    
  )
)

Из этого кода UI у нас есть:

  • два входа:
    • Селектор района (с inputId select_district)
    • Селектор возрастной группы (с inputId select_agegroup)
  • один выход:
    • Эпидкривая (с outputId malaria_epicurve)

Как указывалось ранее эти уникальные имена, которые мы присвоили входам и выходам. Они должны быть уникальными и использоваться для передачи информации между ui и сервером. На нашем сервере мы получаем доступ к нашим входам через синтез input$inputID, а выходы передаются в ui через синтаксис output$output_name. Давайте рассмотрим пример, потому что иначе это сложно понять!

server <- function(input, output, session) {
  
  output$malaria_epicurve <- renderPlot(
    plot_epicurve(malaria_data, district = input$select_district, agegroup = input$select_agegroup)
  )
  
}

Сервер для такого простого приложения на самом деле довольно прост! Вы заметите, что сервер представляет собой функцию с тремя параметрами - input, output и session - пока это не столь важно понимать, но важно придерживаться этой настройки! На нашем сервере у нас только одна задача - сформировать график на основе функции, которую мы создали раньше и входов с сервера. Обратите внимание, что имена объектов входа и вывода совершенно точно совпадают с теми, которые указаны в ui.

Чтобы понять основы того, как сервер реагирует на вводимые пользователем данные, следует отметить, что вывод будет знать (через базовый пакет), когда изменяются вводимые данные, и повторно запускать эту функцию для создания графика каждый раз, когда они изменяются. Обратите внимание, что мы также используем функцию renderPlot() - это одна из семейства функций, относящихся к определенному классу и передающих эти объекты на вывод ui. Существует ряд функций, которые ведут себя аналогичным образом, но необходимо убедиться, что используемая функция соответствует классу объекта, который вы передаете в ui! Например:

  • renderText() - направляет текст в ui
  • renderDataTable - направляет интерактивную таблицу в ui.

Помните, что они также должны соответствовать функции вывода, используемой в ui - так что renderPlot() идет в паре с plotOutput(), а renderText() - с textOutput().

Итак, мы наконец-то создали работающее приложение! Мы можем запустить его, нажав кнопку Run App в правом верхнем углу окна скрипта в Rstudio. Следует отметить, что можно выбрать запуск приложения в браузере по умолчанию (а не в Rstudio), что более точно отразит то, как приложение будет выглядеть для других пользователей.

Любопытно отметить, что в консоли R написано так, будто приложение “слушает! Вот это реагирование!

43.6 Добавление функционала

На данный момент у нас есть работающее приложение, но его функциональность очень мала. Кроме того, мы еще и не коснулись всего, на что способен shiny, так что нам еще многое предстоит узнать! Давайте продолжим развивать наше приложение, добавляя в него дополнительные возможности. Неплохо было бы добавить следующие вещи:

  1. Некоторый пояснительный текст
  2. Кнопку скачивания для нашего графика - это даст пользователю возможность использовать высококачественную версию изображения, которое генерирует приложение
  3. Селектор медицинских организаций
  4. Еще одну страницу информационной панели - это может показать таблицу наших данных.

Мы добавим много нового, но с помощью этого мы сможем узнать о множестве различных функций shiny. О shiny можно узнать очень многое (оно может быть очень продвинутым, но есть надежда, что после того, как пользователи получат представление о том, как его использовать, они смогут более комфортно использовать и внешние источники обучения).

Добавление статического текста

Сначала поговорим о добавлении статического текста в наше приложение shiny. Добавление текста в наше приложение очень просто, если иметь базовое представление о нем. Поскольку статический текст не изменяется в приложении shiny (если вы хотите, чтобы он изменялся, вы можете использовать функции рендеринга текста на сервере!), весь статический текст в shiny обычно добавляется в ui приложения. Мы не будем останавливаться на этом подробно, но вы можете добавить в свой ui множество различных элементов (и даже пользовательских), взаимодействуя с R и HTML и css.

HTML и css - языки, которые явно вовлечены в дизайн интерфейса пользователя. Нам не обязательно в этом хорошо разбираться, но HTML создает объекты в UI (как текстовое поле или таблица), а css, как правило, используется для изменения стиля и эстетики этих объектов. У Shiny есть доступ к широкому спектру тэгов HTML - они присутствуют для объектов, которые ведут себя определенным образом, например, заголовки, абзацы текста, переносы строк, таблицы и т.д. Мы можем использовать некоторые из этих примеров следующим образом:

  • h1() - это тэг заголовка, который автоматически увеличит размер вложенного текста, а также изменит настройки по умолчанию в отношении начертания шрифта, цвета и т.д. (в зависимости от общей тематики приложения). Вы можете получить доступ к все меньшим и меньшим подзаголовкам с помощью h2() до h6(). Применение выглядит следующим образом:

    • h1("my header - section 1")
  • p() - это тэг абзаца, который сделает вложенный текст похожим на текст в теле текста. Этот текст будет автоматически обернут и будет иметь относительно небольшой размер (например, колонтитулы могут быть меньше). Использование выглядит следующим образом:

    • p("This is a larger body of text where I am explaining the function of my app")
  • tags$b() и tags$i() - используются для создания жирного шрифта tags$b() и курсива tags$i() применительно к тексту, который в них заключен!

  • tags$ul(), tags$ol() и tags$li() - это тэги, используемые для создания списков. Их используют в синтаксисе ниже, они позволяют пользователю создать либо упорядоченный список (tags$ol(); т.е. нумерованный), либо неупорядоченный список (tags$ul(), т.е. маркированный список). tags$li() используется для обозначения пунктов в списке вне зависимости от типа используемого списка. например:

tags$ol(
  
  tags$li("Item 1"),
  
  tags$li("Item 2"),
  
  tags$li("Item 3")
  
)
  • br() и hr() - эти тэги создают разрывы линий и горизонтальные линии (с разрывом), соответственно. Используйте их, чтобы разделить разделы вашего приложения и текста! There is no need to pass any items to these tags (parentheses can remain empty).

  • div() - это общий тэг, который может содержать что угодно, и может быть назван как угодно. Как только вы добьетесь прогресса в дизайне ui, вы можете использовать его для компартментализации вашего пользовательского интерфейса (ui), задавать отдельным разделам конкретные стили, а также создавать взаимодействие между сервером и элементами UI. Мы не будем углубляться в это, но об этом стоит знать!

Обратите внимание, что к каждому из этих объектов можно получить доступ через tags$... или для некоторых просто через функцию. По факту это одно и то же, но может быть полезно использовать стиль tags$..., если вы хотите показывать все более конкретно и случайно не записать что-то поверх функции. Это не исчерпывающий список всех имеющихся тэгов. Полный лист тэгов, доступных в shiny, есть тут, и многие можно использовать путем вставки HTML напрямую в ваш ui!

Если вы чувствуете уверенность, вы можете добавить любые элементы стилизации css в ваши тэги HTML с помощью аргумента style в любом из них. Мы не будем углубляться в то, как это работает, но один из советов для тестирования эстетических изменений пользовательского интерфейса - использовать режим HTML-инспектора в chrome (в вашем shiny-приложении, которое вы запускаете в браузере) и самостоятельно редактировать стиль объектов!

Давайте добавим текст в приложение

ui <- fluidPage(

  titlePanel("Malaria facility visualisation app"),

  sidebarLayout(

    sidebarPanel(
         h4("Options"),
         # селектор района
         selectInput(
              inputId = "select_district",
              label = "Select district",
              choices = c(
                   "All",
                   "Spring",
                   "Bolo",
                   "Dingo",
                   "Barnard"
              ),
              selected = "All",
              multiple = TRUE
         ),
         # селектор возрастной группы
         selectInput(
              inputId = "select_agegroup",
              label = "Select age group",
              choices = c(
                   "All ages" = "malaria_tot",
                   "0-4 yrs" = "malaria_rdt_0-4",
                   "5-14 yrs" = "malaria_rdt_5-14",
                   "15+ yrs" = "malaria_rdt_15"
              ), 
              selected = "All",
              multiple = FALSE
         ),
    ),

    mainPanel(
      # эпидкривая здесь
      plotOutput("malaria_epicurve"),
      br(),
      hr(),
      p("Welcome to the malaria facility visualisation app! To use this app, manipulate the widgets on the side to change the epidemic curve according to your preferences! To download a high quality image of the plot you've created, you can also download it with the download button. To see the raw data, use the raw data tab for an interactive form of the table. The data dictionary is as follows:"),
    tags$ul(
      tags$li(tags$b("location_name"), " - the facility that the data were collected at"),
      tags$li(tags$b("data_date"), " - the date the data were collected at"),
      tags$li(tags$b("submitted_daate"), " - the date the data were submitted at"),
      tags$li(tags$b("Province"), " - the province the data were collected at (all 'North' for this dataset)"),
      tags$li(tags$b("District"), " - the district the data were collected at"),
      tags$li(tags$b("age_group"), " - the age group the data were collected for (0-5, 5-14, 15+, and all ages)"),
      tags$li(tags$b("cases_reported"), " - the number of cases reported for the facility/age group on the given date")
    )
    
  )
)
)

Добавление ссылки

Чтобы добавить ссылку на веб-сайт, используйте tags$a() со ссылкой и отобразите текст, как показано ниже. Чтобы был отдельный абзац, поместите внутри p(). Чтобы сделать ссылку из нескольких слов предложения, разбейте приложения на части и используйте для гипертекстовой части tags$a(). Чтобы убедиться, что ссылка откроется в новом окне браузера, добавьте target = "_blank" в качестве аргумента.

tags$a(href = "www.epiRhandbook.com", "Visit our website!")

Добавление кнопки скачивания

Перейдем ко второй из трех функций. Кнопка загрузки - это довольно распространенная вещь, которую можно добавить в приложение, и сделать ее довольно просто. Нам нужно добавить еще один виджет в наш ui и добавить еще один выход на наш сервер для подключения к нему. Мы таже можем ввести реагирующие проводники в этом примере!

Сначала обновим наш ui - это легко, поскольку у shiny есть виджет под названием downloadButton() - давайте зададим для него inputId и подпись.

ui <- fluidPage(

  titlePanel("Malaria facility visualisation app"),

  sidebarLayout(

    sidebarPanel(
         # селектор района
         selectInput(
              inputId = "select_district",
              label = "Select district",
              choices = c(
                   "All",
                   "Spring",
                   "Bolo",
                   "Dingo",
                   "Barnard"
              ),
              selected = "All",
              multiple = FALSE
         ),
         # селектор возрастной группы
         selectInput(
              inputId = "select_agegroup",
              label = "Select age group",
              choices = c(
                   "All ages" = "malaria_tot",
                   "0-4 yrs" = "malaria_rdt_0-4",
                   "5-14 yrs" = "malaria_rdt_5-14",
                   "15+ yrs" = "malaria_rdt_15"
              ), 
              selected = "All",
              multiple = FALSE
         ),
         # горизонтальная линия
         hr(),
         downloadButton(
           outputId = "download_epicurve",
           label = "Download plot"
         )

    ),

    mainPanel(
      # эпидкривая сюда
      plotOutput("malaria_epicurve"),
      br(),
      hr(),
      p("Welcome to the malaria facility visualisation app! To use this app, manipulate the widgets on the side to change the epidemic curve according to your preferences! To download a high quality image of the plot you've created, you can also download it with the download button. To see the raw data, use the raw data tab for an interactive form of the table. The data dictionary is as follows:"),
      tags$ul(
        tags$li(tags$b("location_name"), " - the facility that the data were collected at"),
        tags$li(tags$b("data_date"), " - the date the data were collected at"),
        tags$li(tags$b("submitted_daate"), " - the date the data were submitted at"),
        tags$li(tags$b("Province"), " - the province the data were collected at (all 'North' for this dataset)"),
        tags$li(tags$b("District"), " - the district the data were collected at"),
        tags$li(tags$b("age_group"), " - the age group the data were collected for (0-5, 5-14, 15+, and all ages)"),
        tags$li(tags$b("cases_reported"), " - the number of cases reported for the facility/age group on the given date")
      )
      
    )
    
  )
)

Обратите внимание, что мы также добавили тэг hr() - он добавляет горизонтальную линию, разделяющую виджеты контроля от виджетов скачивания. Это еще один из тэгов HTML, которые мы обсуждали выше.

Теперь, когда готов наш ui, нам нужно добавить серверный компонент. Скачивания выполняются на сервере с помощью функции downloadHandler(). Аналогично нашему графику, нам нужно прикрепить ее к выходу, который имеет тот же inputId, что и кнопка загрузки. Эта функция принимает два аргумента - filename и content - оба этих аргумента являются функциями. Как вы могли догадаться, filename используется, чтобы уточнить имя скачиваемого файла, а content используется для уточнения того, что нужно скачать. content содержит функцию, которую можно использовать для локального сохранения данных - так, если вы загружаете файл csv, вы можете использовать rio::export(). Поскольку мы скачиваем график, мы будем использовать ggplot2::ggsave(). Давайте посмотрим, как нам это запрограммировать (мы пока еще не добавляем это на сервер).

server <- function(input, output, session) {
  
  output$malaria_epicurve <- renderPlot(
    plot_epicurve(malaria_data, district = input$select_district, agegroup = input$select_agegroup)
  )
  
  output$download_epicurve <- downloadHandler(
    filename = function() {
      stringr::str_glue("malaria_epicurve_{input$select_district}.png")
    },
    
    content = function(file) {
      ggsave(file, 
             plot_epicurve(malaria_data, district = input$select_district, agegroup = input$select_agegroup),
             width = 8, height = 5, dpi = 300)
    }
    
  )
  
}

Обратите внимание, что функция content всегда принимает аргумент file, который мы размещаем там, где указывается имя выходного файла. Вы можете также заметить, что мы здесь повторям код - мы используем функцию plot_epicurve() дважды на этом сервере, один раз для скачивания и один - для изображения, которое отображается в приложении. Хотя это не сильно влияет на производительность, это означает, что код для генерации этого графика придется запускать, когда пользователь меняет виджеты, указывающие район и возрастную группу, а также снова, когда вы хотите скачать график. В больших приложениях неоптимальные решения, подобные этому, будут все больше и больше замедлять работу, поэтому полезно узнать, как сделать наше приложение более эффективным в этом смысле. Что было бы более логично, так это иметь возможность запускать код для эпидкривой при изменении районов/возрастных групп, и использовать его для функций renderPlot() и downloadHandler(). Вот зачем нужны реагирующие проводники!

Реагирующие проводники - объекты, которые создаются на сервере shiny реагирующим образом, но они не идут в выходные данные - они просто используются другими частями сервера. Существует ряд разных типов реагирующих проводников, но мы разберем два основных.

1.reactive() - это наиболее простой реагирующий проводник - он будет реагировать, когда любые входы внутри него меняются (то есть наши виджеты групп района/возраста)
2. eventReactive()- это реагирующий проводник, который работает так же, как reactive(), но еще пользователь может задать, какие входные данные заставляют его выдавать результат. Это полезно, если реагирующий проводник требует большого времени обработки, но более подробно мы объясним позже.

Давайте рассмотрим два примера:

malaria_plot_r <- reactive({
  
  plot_epicurve(malaria_data, district = input$select_district, agegroup = input$select_agegroup)
  
})


# выполняется только при изменении селектора районов!
malaria_plot_er <- eventReactive(input$select_district, {
  
  plot_epicurve(malaria_data, district = input$select_district, agegroup = input$select_agegroup)
  
})

Когда мы используем eventReactive(), мы можем уточнить, какие входные данные вызовут выполнение этого фрагмента кода - это пока нам не очень полезно, поэтому пока мы это пропустим. Обратите внимание, что несколько входов нужно указывать с помощью c()

Давайте посмотрим, как интегрировать это в наш код сервера:

server <- function(input, output, session) {
  
  malaria_plot <- reactive({
    plot_epicurve(malaria_data, district = input$select_district, agegroup = input$select_agegroup)
  })
  
  
  
  output$malaria_epicurve <- renderPlot(
    malaria_plot()
  )
  
  output$download_epicurve <- downloadHandler(
    
    filename = function() {
      stringr::str_glue("malaria_epicurve_{input$select_district}.png")
    },
    
    content = function(file) {
      ggsave(file, 
             malaria_plot(),
             width = 8, height = 5, dpi = 300)
    }
    
  )
  
}

Видно, что мы просто обращаемся к выводу реагирующей функции, который мы определили в функциях загрузки и создания графика. Следует отметить один момент, который часто ставит людей в тупик: необходимо использовать выходы реагирующих проводников так, как если бы они были функциями - то есть нужно добавлять пустые скобки в конце них (т.е. malaria_plot() будет правильным, а malaria_plot - нет). Теперь, когда мы добавили это решение, наше приложение стало немного аккуратнее, быстрее и легче поддается изменениям, поскольку весь наш код, выполняющий функцию для эпидкривой, находится в одном месте.

Добавление селектора медицинских организаций

Перейдем к следующему элементу - селектору для конкретных организаций. Мы внедрим в нашу функцию еще один параметр, чтобы иметь возможность передавать его в качестве аргумента из нашего кода. Сначала посмотрим, как это сделать - он работает по тем же принципам, что и другие параметры, которые мы задали. Обновим и протестируем нашу функцию.

plot_epicurve <- function(data, district = "All", agegroup = "malaria_tot", facility = "All") {
  
  if (!("All" %in% district)) {
    data <- data %>%
      filter(District %in% district)
    
    plot_title_district <- stringr::str_glue("{paste0(district, collapse = ', ')} districts")
    
  } else {
    
    plot_title_district <- "all districts"
    
  }
  
  # если нет оставшихся данных, выдать NULL
  if (nrow(data) == 0) {
    
    return(NULL)
  }
  
  data <- data %>%
    filter(age_group == agegroup)
  
  
  # если нет оставшихся данных, выдать NULL
  if (nrow(data) == 0) {
    
    return(NULL)
  }
  
  if (agegroup == "malaria_tot") {
      agegroup_title <- "All ages"
  } else {
    agegroup_title <- stringr::str_glue("{str_remove(agegroup, 'malaria_rdt')} years")
  }
  
    if (!("All" %in% facility)) {
    data <- data %>%
      filter(location_name == facility)
    
    plot_title_facility <- facility
    
  } else {
    
    plot_title_facility <- "all facilities"
    
  }
  
  # если нет оставшихся данных, выдать NULL
  if (nrow(data) == 0) {
    
    return(NULL)
  }

  
  
  ggplot(data, aes(x = data_date, y = cases_reported)) +
    geom_col(width = 1, fill = "darkred") +
    theme_minimal() +
    labs(
      x = "date",
      y = "number of cases",
      title = stringr::str_glue("Malaria cases - {plot_title_district}; {plot_title_facility}"),
      subtitle = agegroup_title
    )
  
  
  
}

давайте протестируем:

plot_epicurve(malaria_data, district = "Spring", agegroup = "malaria_rdt_0-4", facility = "Facility 1")

При наличии всех учреждений в наших данных не очень понятно, какие учреждения соответствуют тем или иным районам, и конечный пользователь тоже этого не знает. Это может сделать работу с приложением довольно неудобной. По этой причине мы должны сделать так, чтобы варианты объектов в пользовательском интерфейсе динамически менялись при смене района - таким образом, одно фильтрует другое! Поскольку у нас так много переменных, которые мы используем в опциях, мы также можем захотеть сгенерировать некоторые опции для пользовательского интерфейса в файле global.R из данных. Например, мы можем добавить этот фрагмент кода в global.R после того, как мы прочитали наши данные:

all_districts <- c("All", unique(malaria_data$District))

# датафрейм имен местности по районам
facility_list <- malaria_data %>%
  group_by(location_name, District) %>%
  summarise() %>% 
  ungroup()

Давайте взглянем на них:

all_districts
[1] "All"     "Spring"  "Bolo"    "Dingo"   "Barnard"
facility_list
# A tibble: 65 × 2
   location_name District
   <chr>         <chr>   
 1 Facility 1    Spring  
 2 Facility 10   Bolo    
 3 Facility 11   Spring  
 4 Facility 12   Dingo   
 5 Facility 13   Bolo    
 6 Facility 14   Dingo   
 7 Facility 15   Barnard 
 8 Facility 16   Barnard 
 9 Facility 17   Barnard 
10 Facility 18   Bolo    
# ℹ 55 more rows

Мы можем передать эти новые переменные в ui без проблем, поскольку они глобально видимы и серверу, и ui! Давайте обновим UI:

ui <- fluidPage(

  titlePanel("Malaria facility visualisation app"),

  sidebarLayout(

    sidebarPanel(
         # селектор района
         selectInput(
              inputId = "select_district",
              label = "Select district",
              choices = all_districts,
              selected = "All",
              multiple = FALSE
         ),
         # селектор возрастной группы
         selectInput(
              inputId = "select_agegroup",
              label = "Select age group",
              choices = c(
                   "All ages" = "malaria_tot",
                   "0-4 yrs" = "malaria_rdt_0-4",
                   "5-14 yrs" = "malaria_rdt_5-14",
                   "15+ yrs" = "malaria_rdt_15"
              ), 
              selected = "All",
              multiple = FALSE
         ),
         # селектор организации
         selectInput(
           inputId = "select_facility",
           label = "Select Facility",
           choices = c("All", facility_list$location_name),
           selected = "All"
         ),
         
         # горизонтальная линия
         hr(),
         downloadButton(
           outputId = "download_epicurve",
           label = "Download plot"
         )

    ),

    mainPanel(
      # эпидкривая сюда
      plotOutput("malaria_epicurve"),
      br(),
      hr(),
      p("Welcome to the malaria facility visualisation app! To use this app, manipulate the widgets on the side to change the epidemic curve according to your preferences! To download a high quality image of the plot you've created, you can also download it with the download button. To see the raw data, use the raw data tab for an interactive form of the table. The data dictionary is as follows:"),
      tags$ul(
        tags$li(tags$b("location_name"), " - the facility that the data were collected at"),
        tags$li(tags$b("data_date"), " - the date the data were collected at"),
        tags$li(tags$b("submitted_daate"), " - the date the data were submitted at"),
        tags$li(tags$b("Province"), " - the province the data were collected at (all 'North' for this dataset)"),
        tags$li(tags$b("District"), " - the district the data were collected at"),
        tags$li(tags$b("age_group"), " - the age group the data were collected for (0-5, 5-14, 15+, and all ages)"),
        tags$li(tags$b("cases_reported"), " - the number of cases reported for the facility/age group on the given date")
      )
      
    )
    
  )
)

Обратите внимание, что теперь мы передаем переменные для выбора вместо того, чтобы жестко кодировать их в ui! Это также может сделать наш код более компактным! Наконец, нам нужно будет обновить сервер. Обновить нашу функцию для включения новых входных данных несложно (достаточно передать их в качестве аргумента новому параметру), но не стоит забывать, что мы также хотим, чтобы пользовательский интерфейс динамически обновлялся, когда пользователь меняет выбранный район. Важно понимать, что мы можем изменить параметры и поведение виджетов, когда приложение запущено, но это надо сделать на сервере. Нам нужно разобраться в новом способе вывода на сервер, чтобы это сделать.

Функции, в которых нам нужно для этого разобраться, называются функции наблюдателя, они похожи на реагирующие функции в том, как они себя ведут. Но есть одно ключевое отличие:

  • Реагирующие функции не влияют на выходные данные напрямую и создают объекты, которые можно увидеть в других местах сервера
  • Функции наблюдателя могут повлиять на выходные данные с сервера, но через побочные эффекты других функций. (Они могут быть использованы и для других целей, но это их основная практическая функция)

Аналогично реагирующим функциям существует два типа функций наблюдателя, они делятся по той же логике, по которой делятся реагирующие функции:

  1. observe() - эта функция выполняется, когда меняются любые входы внутри нее
  2. observeEvent() - эта функция выполняется, когда меняется заданный пользователем вход

Нам также нужно понять функции shiny, которые обновляют виджеты. Их достаточно просто выполнять - сначала они берут объект session из функции сервера (это пока не обязательно понимать), а затем inputId функции, которую нужно менять. Мы затем передаем новые версии всех параметров, которые уже взяты selectInput() - они будут автоматически обновлены в виджете.

Рассмотрим отдельный пример того, как мы можем использовать это в нашем сервере. Когда пользователь меняет район, мы хотим отфильтровать нашу таблицу объектов по району и обновить варианты так, чтобы они отражали только те, которые доступны в этом районе (и опцию для всех учреждений)

observe({
  
  if (input$select_district == "All") {
    new_choices <- facility_list$location_name
  } else {
    new_choices <- facility_list %>%
      filter(District == input$select_district) %>%
      pull(location_name)
  }
  
  new_choices <- c("All", new_choices)
  
  updateSelectInput(session, inputId = "select_facility",
                    choices = new_choices)
  
})

И все! Мы можем это добавить на наш сервер и такое поведение теперь сработает Вот как должен выглядеть наш новый сервер:

server <- function(input, output, session) {
  
  malaria_plot <- reactive({
    plot_epicurve(malaria_data, district = input$select_district, agegroup = input$select_agegroup, facility = input$select_facility)
  })
  
  
  
  observe({
    
    if (input$select_district == "All") {
      new_choices <- facility_list$location_name
    } else {
      new_choices <- facility_list %>%
        filter(District == input$select_district) %>%
        pull(location_name)
    }
    
    new_choices <- c("All", new_choices)
    
    updateSelectInput(session, inputId = "select_facility",
                      choices = new_choices)
    
  })
  
  
  output$malaria_epicurve <- renderPlot(
    malaria_plot()
  )
  
  output$download_epicurve <- downloadHandler(
    
    filename = function() {
      stringr::str_glue("malaria_epicurve_{input$select_district}.png")
    },
    
    content = function(file) {
      ggsave(file, 
             malaria_plot(),
             width = 8, height = 5, dpi = 300)
    }
    
  )
  
  
  
}

Добавление еще одной вкладки с таблицей

Теперь перейдем к последнему компоненту, который мы хотим добавить в наше приложение. Мы хотим разделить наш пользовательский интерфейс на две вкладки, одна из которых будет содержать интерактивную таблицу, в которой пользователь сможет увидеть данные, на основе которых он строит эпидемическую кривую. Для этого мы можем использовать упакованные элементы ui, которые идут в комплекте с shiny, относящимися к вкладкам. На базовом уровне мы можем заключить большую часть нашей основной панели в такую общую структуру:

# ... вся остальная часть ui

mainPanel(
  
  tabsetPanel(
    type = "tabs",
    tabPanel(
      "Epidemic Curves",
      ...
    ),
    tabPanel(
      "Data",
      ...
    )
  )
)

Применим это к нашему ui. Нам также нужно использовать здесь пакет DT - это замечательный пакет для создания интерактивных таблиц из существующих данных. Мы видим, как он используется для DT::datatableOutput() в этом примере.

ui <- fluidPage(
     
     titlePanel("Malaria facility visualisation app"),
     
     sidebarLayout(
          
          sidebarPanel(
               # селектор района
               selectInput(
                    inputId = "select_district",
                    label = "Select district",
                    choices = all_districts,
                    selected = "All",
                    multiple = FALSE
               ),
               # селектор возрастной группы
               selectInput(
                    inputId = "select_agegroup",
                    label = "Select age group",
                    choices = c(
                         "All ages" = "malaria_tot",
                         "0-4 yrs" = "malaria_rdt_0-4",
                         "5-14 yrs" = "malaria_rdt_5-14",
                         "15+ yrs" = "malaria_rdt_15"
                    ), 
                    selected = "All",
                    multiple = FALSE
               ),
               # селектор организации
               selectInput(
                    inputId = "select_facility",
                    label = "Select Facility",
                    choices = c("All", facility_list$location_name),
                    selected = "All"
               ),
               
               # горизонтальная линия
               hr(),
               downloadButton(
                    outputId = "download_epicurve",
                    label = "Download plot"
               )
               
          ),
          
          mainPanel(
               tabsetPanel(
                    type = "tabs",
                    tabPanel(
                         "Epidemic Curves",
                         plotOutput("malaria_epicurve")
                    ),
                    tabPanel(
                         "Data",
                         DT::dataTableOutput("raw_data")
                    )
               ),
               br(),
               hr(),
               p("Welcome to the malaria facility visualisation app! To use this app, manipulate the widgets on the side to change the epidemic curve according to your preferences! To download a high quality image of the plot you've created, you can also download it with the download button. To see the raw data, use the raw data tab for an interactive form of the table. The data dictionary is as follows:"),
               tags$ul(
                    tags$li(tags$b("location_name"), " - the facility that the data were collected at"),
                    tags$li(tags$b("data_date"), " - the date the data were collected at"),
                    tags$li(tags$b("submitted_daate"), " - the date the data were submitted at"),
                    tags$li(tags$b("Province"), " - the province the data were collected at (all 'North' for this dataset)"),
                    tags$li(tags$b("District"), " - the district the data were collected at"),
                    tags$li(tags$b("age_group"), " - the age group the data were collected for (0-5, 5-14, 15+, and all ages)"),
                    tags$li(tags$b("cases_reported"), " - the number of cases reported for the facility/age group on the given date")
               )
               
               
          )
     )
)

Теперь наше приложение структурировано по вкладкам! Давайте внесем необходимые правки на сервере. Поскольку нам не нужны манипуляции с набором данных до его обработки, все будет очень просто - мы просто обрабатываем набор данных malaria_data через DT::renderDT() в ui!

server <- function(input, output, session) {
  
  malaria_plot <- reactive({
    plot_epicurve(malaria_data, district = input$select_district, agegroup = input$select_agegroup, facility = input$select_facility)
  })
  
  
  
  observe({
    
    if (input$select_district == "All") {
      new_choices <- facility_list$location_name
    } else {
      new_choices <- facility_list %>%
        filter(District == input$select_district) %>%
        pull(location_name)
    }
    
    new_choices <- c("All", new_choices)
    
    updateSelectInput(session, inputId = "select_facility",
                      choices = new_choices)
    
  })
  
  
  output$malaria_epicurve <- renderPlot(
    malaria_plot()
  )
  
  output$download_epicurve <- downloadHandler(
    
    filename = function() {
      stringr::str_glue("malaria_epicurve_{input$select_district}.png")
    },
    
    content = function(file) {
      ggsave(file, 
             malaria_plot(),
             width = 8, height = 5, dpi = 300)
    }
    
  )
  
  # обработка таблицы данных в ui
  output$raw_data <- DT::renderDT(
    malaria_data
  )
  
  
}

43.7 Предоставление доступа к приложениям shiny

Теперь, когда вы разработали свое приложение, вы наверняка хотите поделиться им с другими - ведь это главное преимущество shiny! Для этого можно поделиться кодом напрямую, а можно опубликовать его на сервере. Если мы поделимся кодом, другие смогут увидеть то, что вы сделали, и использовать это, но это сводит на нет одно из главных преимуществ shiny - оно может устранить необходимость для конечных пользователей пользоваться R. По этой причине, если вы делитесь своим приложением с пользователями, не имеющими опыта работы с R, гораздо проще поделиться приложением, опубликованным на сервере.

Если вы предпочитаете поделиться кодом, вы должны создать файл .zip file с приложением, а еще лучше опубликовать ваше приложение на github и добавить совместно работающих людей. См. дополнительную информацию в разделе по github.

Однако если мы публикуем приложение в Интернете, то нам необходимо проделать немного больше работы. В конечном итоге мы хотим, чтобы доступ к вашему приложению можно было получить по URL-адресу в Интернете, чтобы другие пользователи могли быстро и легко получить доступ к нему. К сожалению, чтобы опубликовать приложение на сервере, необходимо иметь доступ к серверу, на котором оно будет опубликовано! Для этого существует несколько вариантов хостинга:

  • shinyapps.io: Это самое простое место для публикации приложений shiny, поскольку оно требует наименьшего объема работ по настройке и имеет несколько бесплатных, но ограниченных лицензий.

  • RStudio Connect: это гораздо более мощная версия R-сервера, которая может выполнять множество операций, включая публикацию приложений shiny. Однако он сложнее в использовании и не рекомендуется для начинающих пользователей.

В этом документе мы используем shinyapps.io, поскольку он проще для новых пользователей. Для начала можно создать бесплатную учетную запись - при необходимости предлагаются различные тарифные планы для серверных линий. Чем больше пользователей вы планируете иметь, тем дороже может быть ваш тарифный план, поэтому учитывайте этот момент. Если вы хотите создать что-то для небольшого круга пользователей, бесплатная лицензия может быть вполне подходящей, но для публичного приложения может потребоваться больше лицензий.

Прежде всего мы должны убедиться, что наше приложение пригодно для публикации на сервере. В приложении необходимо перезапустить сессию R и убедиться, что оно запускается без дополнительного кода. Это важно, поскольку приложение, требующее загрузки пакетов или чтения данных, не определенных в коде приложения, не будет работать на сервере. Также обратите внимание, что в приложении не должно быть конкретных путей к файлам - они все будут недействительны в условиях сервера - использование пакета here очень хорошо решает эту проблему. Наконец, если вы читаете данные из источника, который требует авторизации пользователя, например, с сервера вашей организации, как правило, это не будет работать на сервере. Вам нужно будет поговорить с вашим ИТ департаментом, чтобы решить, как включить сервер shiny в белый список.

регистрация учетной записи

Как только у вас будет учетная запись, вы можете найти на страницу токенов в Accounts. Здесь вам нужно добавить новый токен - он будет использован для размещения вашего приложения.

Отсюда следует, что url вашей учетной записи будет отражать название вашего приложения - так что если ваше приложение называется my_app, в url будет записано xxx.io/my_app/. Внимательно выбирайте название вашего приложения! Теперь, когда все готово, нажмите кнопку разместить - в случае успеха приложение будет запущено на выбранном вами веб-url!

что-то о создании приложений в документах?

43.8 Дополнительное чтение

До сих пор мы рассмотрели множество аспектов работы с shiny и лишь слегка коснулись того, что предлагает shiny. Хотя это руководство служит введением, для полного понимания shiny необходимо узнать еще много нового. Вам следует начать создавать приложения и постепенно добавлять все больше и больше функциональности.

43.9 Рекомендованные пакеты для расширения

Ниже представлена подборка высококачественных расширений для shiny, которые помогут вам получить гораздо больше пользы от shiny. Список не в порядке приоритетности:

  • shinyWidgets - этот пакет даст вам гораздо больше виджетов, которые вы можете использовать в приложении. Выполните shinyWidgets::shinyWidgetsGallery(), чтобы увидеть перечень доступных в этом пакете виджетов. См. примеры тут

  • shinyjs - Это отличный пакет, позволяющий значительно расширить возможности shiny с помощью ряда javascript. Возможности применения этого пакета варьируются от очень простых до весьма продвинутых, но для начала лучше использовать его для простых манипуляций с интерфейсом, таких как скрытие/показ элементов или включение/выключение кнопок. Детали см. тут

  • shinydashboard - Этот пакет значительно расширяет возможности использования пользовательского интерфейса в shiny, в частности, позволяет создавать сложные информационные панели с различными вариантами компоновки. См. подробнее тут

  • shinydashboardPlus - дает еще больше свойств в структуре shinydashboard! См. детали тут

  • shinythemes - изменения в схеме css по умолчанию для вашего приложения shiny с широким спектров предварительно настроенных шаблонов! См. детали тут

Существует также ряд пакетов, которые могут быть использованы для создания интерактивных выводов, совместимых с shiny.

  • DT является наполовину интегрированным в базовый-shiny, но предоставляет отличный набор функций для создания интерактивных таблиц.

  • plotly - это пакет для создания интерактивных графиков, которыми пользователь может манипулировать в приложении. Вы можете также конвертировать ваш график в интерактивные версии с помощью plotly::ggplotly()! В качестве альтернатив также замечательно подходят dygraphs и highcharter.

43.10 Рекомендованные источники