Интерактивные таблицы средствами языка R

пример создания интерактивных таблиц

Введение

Табличный способ является одним из основных способов структурирования и представления данных. В отличие от графиков, таблицы, как правило, не дают быстрого визуального представления о данных. Тем не менее, таблицы используются для того чтобы показать точные значения данных. Правильно созданная таблица позволяет найти определенное значение, понять закономерности или найти выбросы в данных.

Мы рассмотрим представление данных средствами языка программирования R в виде интерактивных таблиц. Одним из самых простых способов для формирования интерактивных таблиц в R является библиотека DT, которая позволяет делать удобную сортировку и фильтрацию данных, а также предоставляет массу других дополнительных возможностей. Таблицы, которые мы рассмотрим здесь, построены в относительно новой, но уже достаточно насыщенной возможностями библиотеке reactable основанной на JavaScript-библиотеке React Table и разработанной с помощью reactR.

В качестве примера рассмотрим данные по пожарам1, нормированные на численность населения в соответствующем субъекте Российской Федерации2.

Отметим, что таблица выше не является официальным статистическим отчетом и служит исключительно для демонстрации работы библиотеки3. Основные показатели, характеризующие состояние пожарной безопасности в Российской Федерации ежегодно публикуются ФГБУ ВНИИПО МЧС РОССИИ в статистических сборниках ПОЖАРЫ И ПОЖАРНАЯ БЕЗОПАСНОСТЬ.

Некоторые особенности и возможности интерактивных таблиц

  • Возможность живой сортировки значений по столбцам (можно указать исходное направление сортировки).

  • Живой поиск, позволяющий выделять только строки с нужными значениями и делать автоматический пересчет итоговых суммарных значений.

  • Регулировка количества строк на странице и диапазона значений.

  • Группировка и агрегирование строк с автоматическим выделением границ таблицы.

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

  • Использование JavaScript для улучшения стилизации и внедрение HTML-виджетов в таблицы.

  • Использование CSS-стилей.

  • Богатые возможности для русификации.

  • Интегрирование с Shiny (интерактивной средой для создания веб-приложений и дэшбордов) и Leaflet (библиотекой предназначенной для отображения карт на веб-сайтах).

Посторение интерактивных таблиц

Код для построения рассмотренной выше таблицы по пожарам довольно большой, поэтому вместо полного листинга приведем основные идеи и принципы создания интерактивных таблиц в reactable на основе небольших примеров описанных на странице библиотеки.

Загрузка библиотек

Загрузим необходимые библиотеки.

library(tidyverse)
library(magrittr)
library(reactable)
library(htmltools)

Базовая таблица

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

fires_reactable <- 
fires_counted %>% 
  as_tibble() %>% 
  head(20)  

fires_reactable
## # A tibble: 20 x 7
##    regions                         FO    `2017` `2018` `2019` `2020` total
##    <fct>                           <fct>  <dbl>  <dbl>  <dbl>  <dbl> <dbl>
##  1 Алтайский край                  СФО       52     54     62     56   224
##  2 Амурская область                ДФО       64     77     68     51   261
##  3 Архангельская область           СЗФО      11     12     12     11    46
##  4 Астраханская область            ЮФО       52     52     48     54   205
##  5 Белгородская область            ЦФО       11     12     18     17    59
##  6 Брянская область                ЦФО       51     44     47     39   182
##  7 Владимирская область            ЦФО       25     35     32     25   118
##  8 Волгоградская область           ЮФО       50     45     37     47   179
##  9 Вологодская область             СЗФО      25     25     19     16    86
## 10 Воронежская область             ЦФО       30     34     32     45   141
## 11 г. Москва                       ЦФО        8      8      8      6    29
## 12 г. Санкт-Петербург              СЗФО      22     27     20     17    86
## 13 г. Севастополь                  ЮФО       34     28     28     24   114
## 14 Еврейская автономная область    ДФО       95    109    137     91   431
## 15 Забайкальский край              ДФО       68     62     95     65   289
## 16 Ивановская область              ЦФО       28     35     40     30   133
## 17 Иркутская область               СФО       34     31     30     27   122
## 18 Кабардино-Балкарская Республика СКФО      21     18     18     21    78
## 19 Калининградская область         СЗФО      33     43     46     35   156
## 20 Калужская область               ЦФО       35     47     35     28   144

Для отображения интерактивной таблицы служит команда reactable(). Столбцы такой таблицы можно сортировать нажатием (или кликом) на них.

reactable(fires_reactable)

Русификация таблиц

Одна из ключевых возможностей отличающих reacable это, несомненно, возможность настройки языковых опций таблицы. Например, в библиотеке DT русификация делается несколько сложнее, что также описано на странице Stack Overflow.

# русификация reactable таблиц
options(reactable.language = reactableLang(
  pageSizeOptions   = "показано {rows} значений",
  pageInfo          = "Диапазон: с {rowStart} по {rowEnd} из {rows} регионов",
  pagePrevious      = "назад",
  pageNext          = "вперед",
  searchPlaceholder = "Поиск...",
  noData            = "Значения не найдены"
))

Глобальные настройки таблицы

Таблицы в reactable имеют огромное количество разнообразных настроек, причем для более глубокой настройки используется JavaScript. Приведем ниже некоторые возможности на примере.

year_cols <- c("2017", "2018", "2019", "2020")

reactable(fires_reactable,
          # группировка и название объединенных столбцов
          columnGroups = list(
            colGroup(name = "Количество пожаров приведенное на 10 000 человек населения", 
                     columns = year_cols)
          ),
          # строка поиска
          searchable = TRUE,
          # исходная глобальная сортировка по умолчанию
          defaultSortOrder = "desc",
          # возможность изменять количество строк на странице
          showPageSizeOptions = TRUE,
          # подсветка при наведении на строку
          highlight = TRUE,
          # отстутствие границы между строками
          borderless = TRUE,
          # на всю ширину / фиксированная ширина
          fullWidth = TRUE,
          # столбцы исходной сортировки и направление сортировки столбцов
          defaultSorted = list(FO = "asc"),
          # исходное количество строк на странице
          defaultPageSize = 10,
          # стилизация заголовков столбцов
          theme = reactableTheme(
            headerStyle = list(
              "&:hover[aria-sort]" = list(background = "hsl(0, 0%, 96%)"),
              "&[aria-sort='ascending'], 
               &[aria-sort='descending']" = list(background = "hsl(0, 0%, 96%)"),
              borderColor = "#555"
            )
          ),
          # стилизация: разграничивание между группами при сортировке по ФО
          rowStyle = JS("
                function(rowInfo, state) {
                        // игнорирование строк заполнения
                        if (!rowInfo) return
                        
                        // горизонтальные разделители между группами
                        var firstSorted = state.sorted[0]
                        if (firstSorted && firstSorted.id === 'FO') {
                        var nextRow = state.pageRows[rowInfo.viewIndex + 1]
                        if (nextRow && rowInfo.row['FO'] !== nextRow['FO']) {
                        // добавление прямоугольной тени, 
                        // чтобы добавить границу 2 пикселя 
                        return { boxShadow: 'inset 0 -2px 0 rgba(0, 0, 0, 0.1)' }
                        }
                        }
                        }
                        "),
          # глобальный стиль: шрифт для подстрочной суммы
          defaultColDef = colDef(
            footerStyle = list(fontWeight = "bold"), 
          # разделитель для тысяч пробелом
            format      = colFormat(locales = "ru-RU", separators = TRUE))
         )

Настройка столбцов в таблицах

Для настройки столбцов в reactable-таблицах (например, изменения имени) используется функция colDef() подробно описанная на соответствующей странице. Приведем простой пример настройки столбцов на базе известного датасета Iris.

set.seed(2021)

reactable(
  sample_n(iris, 10),
  columns = list(
    Sepal.Length = colDef(name = "Длина чашелистика"),
    Sepal.Width  = colDef(name = "Ширина чашелистика"),
    Petal.Length = colDef(show = FALSE),
    Petal.Width  = colDef(name = "Ширина лепестка"),
    Species      = colDef(name = "Вид", 
                          align    = "right",
                          filterable = TRUE)
  )
)

Возможности colDef() очень богатые: можно менять выравнивание конкретного столбца, его ширину указав значение maxWidth, изменить форматирование столбца добавив CSS-стиль и т.д.

Тепловые карты

Известно, что цвет воспринимается гораздо быстрее чем численные значения, поэтому имеет смысл иногда представлять данные в виде так называемых “тепловых карт”. Для создания тепловой карты необходима некоторая подготовка.

# функция цвета
make_color_pal <- function(colors, bias = 1) {
  get_color <- colorRamp(colors, bias = bias)
  function(x) rgb(get_color(x), maxColorValue = 255)
}

# наша палитра
good_color <- make_color_pal(c("#7fb7d7", "#ffffbf", "#fc8d59"), bias = 2)

# максимум и минимум значений в таблице
max_val <-
fires_reactable %>%
  summarise(across(where(is.numeric), ~ max(.x, na.rm = TRUE)))
max_val <- max(max_val %>% dplyr::select(-total))

min_val <-
  fires_reactable %>%
  summarise(across(where(is.numeric), ~ min(.x, na.rm = TRUE)))
min_val <- min(min_val %>% dplyr::select(-total))

# стиль столбца с учетом цвета
my_style_fires <- function(value) {
              if (!is.numeric(value)) return()
              normalized <- (value - min_val) / (max_val - min_val)
              color <- good_color(normalized)
              list(background = color)
            }

Приведем пример тепловой таблицы созданной с помощью собственного стиля.

reactable(
  fires_reactable,
  columns = list(
    regions = colDef(name = "Субъект РФ",       # заголовок для субъекта РФ
                              minWidth = 200,
                              defaultSortOrder = "asc"),
    FO      = colDef(name = "ФО",               # заголовок для ФО
                              align = "center", # выравнивание по центру
                              minWidth = 70,
                              defaultSortOrder = "asc"),
    total   = colDef(name = "Сумма",
                     defaultSortOrder = "desc"),
    `2017`  = colDef(style = my_style_fires),
    `2018`  = colDef(style = my_style_fires),
    `2019`  = colDef(style = my_style_fires),
    `2020`  = colDef(style = my_style_fires)
  )
)

Группировка ячеек

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

reactable(fires_reactable, 
          groupBy = "FO",
          columns = list(
          `2017`  = colDef(aggregate = "count",
                           name = "2017 (кол-во)"),
          `2018`  = colDef(aggregate = "unique",
                           name = "2018 (уник)"),
          `2019`  = colDef(aggregate = "median",
                           name = "2019 (медиан)"),
          `2020`  = colDef(show = FALSE),
          total   = colDef(name = "Сумма (макс)", 
                           aggregate = "max"),
          FO      = colDef(name = "Фед. округ"),
          regions = colDef(name = "Субъект РФ")
          )
)

Динамический пересчет суммы

Библиотека reactable путем удачного сочетания с JavaScript позволяет делать таблицы с динамической визуализацией в виде итоговой сноски, например, при наборе фильтрующего значения может происходить автоматический пересчет суммы. Скажем, если в поле поиска таблицы ниже ввести ЦФО, то останутся субъекты РФ и сумма значений только по Центральному федеральному округу.

reactable(
  fires_reactable,
  searchable      = TRUE,
  highlight       = TRUE,
  borderless      = TRUE,
  defaultPageSize = 5,
  theme = reactableTheme(searchInputStyle = list(width = "100%")),
  columns = list(
    regions = colDef(name     = "Субъект РФ",
                     minWidth = 170,
                     footer   = "Всего:"),
    FO      = colDef(name     = "ФО",      
                     align    = "center", 
                     minWidth = 80),
    total = colDef(name = "Сумма",
      # разделитель для тысяч пробелом
      format = colFormat(locales = "ru-RU", separators = TRUE),
      # динамический пересчет суммы при поиске
      footer = JS("function(colInfo) {var total = 0
                                          colInfo.data.forEach(function(row) {
                                          total += row[colInfo.column.id]
                                          })
                                          return total.toLocaleString('ru-RU')
                                          }")
    )
  ),
  # глобальный стиль: шрифт для подстрочной суммы
  defaultColDef = colDef(
    footerStyle = list(fontWeight = "bold"),
    # разделитель для тысяч пробелом
    format      = colFormat(locales = "ru-RU", separators = TRUE)
  )
)

Условное форматирование

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

fires_change <- 
fires_reactable %>% 
  mutate(change = `2020` - `2017`) %>% 
  select(-c(`2018`:`2019`), 
         -total)

reactable(fires_change, 
          highlight     = TRUE,
          defaultSorted = list(regions = "asc"),
          columns       = list(
  regions = colDef(name       = "Субъект РФ",
                     minWidth = 170),
    FO    = colDef(name       = "Фед. округ",      
                     align    = "center", 
                     minWidth = 80),
  change = colDef(
    name = "Динамика",
    defaultSortOrder = "desc",
    cell = function(value) {
      if (value > 0) paste0("+", value) else value
    },
    style = function(value) {
      color <- if (value > 0) {
        "#e00000"
      } else if (value < 0) {
        "#008000"
      }
      list(fontWeight = 600, color = color)
    }
  )
))

Заключение

В статье был сделан обзор некоторых инструментов по созданию таблиц в библиотеке reactable:

  • формирование основы таблицы;
  • возможности глобальных настроек и настроек столбцов;
  • дополнительные возможности для создания тепловых карт;
  • группировка и аггрегирование ячеек;
  • некоторые JavaScript опции.

Всесторонне описать все возможности создания интерактивных таблиц на reactable в рамках одной статьи очень сложно. Например, мы не затронули CSS-стили, изображения, HTML-виджеты, внедрение reactable в Shiny и Leaflet и многое другое. Как было сказано, страница библиотеки содержит большое количество материала с примерами и замечательными демонстрациями включая полный код и описание.

Подводя итог, отметим что базовые принципы построения эффективных таблиц, которые можно взять за основу для презентации своих данных, описаны во многих источниках, например, в статье Ten Guidelines for Better Tables. Основные тезисы этой статьи можно посмотреть в твите (на английском языке) ниже.


  1. Здесь рассмотрены пожары, учет которых ведется на основании Приказа МЧС России от 24.12.2018 №625 “О формировании электронных баз данных учета пожаров и их последствий”.↩︎

  2. Нормирование по отношению к численности населения производилось на основе статистических данных statdata.ru на 1 января 2021 года. Оценку численности постоянного населения РФ можно также посмотреть на странице Демографическая ситуация по субъектам РФ.↩︎

  3. Это связано, например, с тем, что базы в ГУ МЧС России данных могут несколько различаться по объему с базами данных ВНИИПО МЧС России.↩︎

Евгений Матеров
Евгений Матеров
Зав. кафедрой физики, математики и информационных технологий

Область моих научных интересов включает в себя Data Science, машинное обучение, язык программирования R.

Похожие