├── .Rprofile ├── .gitignore ├── R ├── table-logic-character_data_fetcher.R ├── table-logic-character_data_fetcher_factory.R ├── table-logic-character_data_fetcher_in_memory_impl.R ├── table-logic-character_data_fetcher_paginated_impl.R ├── table-mod_characters_table.R ├── table-mod_filters.R ├── table-mod_pagination_controls.R ├── table-ui-character_image.R ├── table-ui-gender_info.R └── table-ui-status_badge.R ├── README.md ├── app.R ├── config.yml ├── gifs ├── added-character-image.png ├── added-colors.png ├── added-finishing-touches.png ├── added-hierarchy.png ├── added-icons.png ├── app-showcase.gif ├── final-table.png ├── no-pagination-case.gif ├── pagination-case.gif ├── raw-shiny-table.gif └── table-raw.png ├── renv.lock ├── renv ├── .gitignore ├── activate.R └── settings.dcf ├── rsconnect └── shinyapps.io │ └── rszymanski │ └── table-contest.dcf ├── styles └── main.scss ├── table-contest.Rproj └── www └── main.css /.Rprofile: -------------------------------------------------------------------------------- 1 | source("renv/activate.R") 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .Rproj.user 2 | .Rhistory 3 | .RData 4 | .Ruserdata 5 | -------------------------------------------------------------------------------- /R/table-logic-character_data_fetcher.R: -------------------------------------------------------------------------------- 1 | CharacterDataFetcher <- R6::R6Class( 2 | classname = "CharacterDataFetcher", 3 | public = list( 4 | fetch_page = function(page_number, filter_settings) { 5 | 6 | } 7 | ) 8 | ) -------------------------------------------------------------------------------- /R/table-logic-character_data_fetcher_factory.R: -------------------------------------------------------------------------------- 1 | CharacterDataFetcherFactory <- R6::R6Class( 2 | classname = "CharacterDataFetcherFactory", 3 | public = list( 4 | initialize = function(api_url) { 5 | private$api_url <- api_url 6 | invisible(self) 7 | }, 8 | create = function(data_fetcher_type) { 9 | switch( 10 | data_fetcher_type, 11 | paginated = CharacterDataFetcherPaginatedImpl$new(private$api_url), 12 | in_memory = CharacterDataFetcherInMemoryImpl$new(private$api_url) 13 | ) 14 | } 15 | ), 16 | private = list( 17 | api_url = NA 18 | ) 19 | ) -------------------------------------------------------------------------------- /R/table-logic-character_data_fetcher_in_memory_impl.R: -------------------------------------------------------------------------------- 1 | CharacterDataFetcherInMemoryImpl <- R6::R6Class( 2 | classname = "CharacterDataFetcherInMemoryImpl", 3 | inherit = CharacterDataFetcher, 4 | public = list( 5 | initialize = function(api_url) { 6 | private$api_url <- api_url 7 | }, 8 | fetch_page = function(page_number, filter_settings) { 9 | private$fetch_data(filter_settings) 10 | 11 | data_page <- private$in_memory_data$data %>% 12 | head(page_number * 20) %>% 13 | tail(20) 14 | 15 | list( 16 | data = data_page, 17 | total_pages = private$in_memory_data$total_pages 18 | ) 19 | } 20 | ), 21 | private = list( 22 | api_url = NA, 23 | current_filter_query = "", 24 | in_memory_data = NA, 25 | fetch_single_page = function(page_number, filter_query) { 26 | url <- glue::glue("{private$api_url}?page={page_number}&{filter_query}") 27 | api_response <- httr::GET(url = url) 28 | response_content <- httr::content(api_response) 29 | 30 | retrieved_data <- data.table::rbindlist( 31 | lapply( 32 | response_content$results, 33 | function(character) { 34 | data.frame( 35 | image = character$image, 36 | name = character$name, 37 | gender = character$gender, 38 | status = character$status, 39 | species = character$species, 40 | type = character$type 41 | ) 42 | } 43 | ) 44 | ) 45 | 46 | list( 47 | total_pages = response_content$info$pages, 48 | data = retrieved_data, 49 | total_pages = response_content$info$count, 50 | next_page_url = response_content$info$`next` 51 | ) 52 | }, 53 | fetch_data = function(filter_settings) { 54 | filter_settings <- Filter(function(setting) setting != "", filter_settings) 55 | filter_query <- sprintf( 56 | fmt = "%s=%s", 57 | names(filter_settings), 58 | filter_settings 59 | ) %>% paste(collapse = "&") 60 | 61 | if (is.na(private$in_memory_data) || private$current_filter_query != filter_query) { 62 | private$current_filter_query <- filter_query 63 | first_page_data <- private$fetch_single_page( 64 | page_number = 1, filter_query = filter_query 65 | ) 66 | 67 | retrieved_data <- lapply(seq_len(first_page_data$total_pages), function(page_number) { 68 | private$fetch_single_page(page_number, filter_query)$data 69 | }) %>% data.table::rbindlist() 70 | 71 | private$in_memory_data <- list( 72 | data = retrieved_data, 73 | total_pages = first_page_data$total_pages 74 | ) 75 | } 76 | } 77 | ) 78 | ) -------------------------------------------------------------------------------- /R/table-logic-character_data_fetcher_paginated_impl.R: -------------------------------------------------------------------------------- 1 | CharacterDataFetcherPaginatedImpl <- R6::R6Class( 2 | classname = "CharacterDataFetcherPaginatedImpl", 3 | inherit = CharacterDataFetcher, 4 | public = list( 5 | initialize = function(api_url) { 6 | private$api_url <- api_url 7 | }, 8 | fetch_page = function(page_number, filter_settings) { 9 | filter_settings <- Filter(function(setting) setting != "", filter_settings) 10 | filter_query <- sprintf( 11 | fmt = "%s=%s", 12 | names(filter_settings), 13 | filter_settings 14 | ) %>% paste(collapse = "&") 15 | 16 | url <- glue::glue("{private$api_url}?page={page_number}&{filter_query}") 17 | api_response <- httr::GET(url = url) 18 | response_content <- httr::content(api_response) 19 | 20 | retrieved_data <- data.table::rbindlist( 21 | lapply( 22 | response_content$results, 23 | function(character) { 24 | data.frame( 25 | image = character$image, 26 | name = character$name, 27 | gender = character$gender, 28 | status = character$status, 29 | species = character$species, 30 | type = character$type 31 | ) 32 | } 33 | ) 34 | ) 35 | 36 | list( 37 | total_pages = response_content$info$pages, 38 | data = retrieved_data, 39 | page_count = response_content$info$count, 40 | next_page_url = response_content$info$`next` 41 | ) 42 | } 43 | ), 44 | private = list( 45 | api_url = NA 46 | ) 47 | ) -------------------------------------------------------------------------------- /R/table-mod_characters_table.R: -------------------------------------------------------------------------------- 1 | mod_characters_table_ui <- function(id, width, height) { 2 | ns <- shiny::NS(id) 3 | shiny::tagList( 4 | reactable::reactableOutput( 5 | outputId = ns("data_table"), 6 | width = width, 7 | height = height 8 | ) 9 | ) 10 | } 11 | 12 | mod_characters_table_server <- function(id, current_data) { 13 | shiny::moduleServer( 14 | id = id, 15 | function(input, output, session) { 16 | output$data_table <- reactable::renderReactable({ 17 | shiny::req(current_data()) 18 | shiny::req(nrow(current_data()$data) > 0) 19 | 20 | create_character_table( 21 | table_data = current_data()$data 22 | ) 23 | }) 24 | } 25 | ) 26 | } 27 | 28 | create_character_table <- function(table_data) { 29 | reactable::reactable( 30 | data = table_data, 31 | height = "70vh", 32 | defaultColDef = reactable::colDef( 33 | headerClass = "character-table-header" 34 | ), 35 | sortable = FALSE, 36 | columns = list( 37 | image = reactable::colDef( 38 | name = "", 39 | maxWidth = 88, 40 | cell = create_character_image 41 | ), 42 | name = reactable::colDef( 43 | name = "Name", 44 | cell = function(value, row_index) { 45 | shiny::div( 46 | shiny::div( 47 | class = "name-container", 48 | value 49 | ), 50 | shiny::div( 51 | class = "species-container", 52 | table_data[row_index, ]$species 53 | ), 54 | shiny::div( 55 | class = "type-container", 56 | table_data[row_index, ]$type 57 | ) 58 | ) 59 | } 60 | ), 61 | type = reactable::colDef(show = FALSE), 62 | species = reactable::colDef(show = FALSE), 63 | gender = reactable::colDef( 64 | name = "Gender", 65 | cell = create_gender_column_content 66 | ), 67 | status = reactable::colDef( 68 | name = "Status", 69 | cell = create_status_badge 70 | ) 71 | ), 72 | pagination = FALSE, 73 | theme = reactable::reactableTheme( 74 | cellStyle = list( 75 | display = "flex", 76 | flexDirection = "column", 77 | justifyContent = "center" 78 | ) 79 | ) 80 | ) 81 | } -------------------------------------------------------------------------------- /R/table-mod_filters.R: -------------------------------------------------------------------------------- 1 | mod_filters_ui <- function(id) { 2 | ns <- shiny::NS(id) 3 | shiny::div( 4 | class = "filters-container", 5 | shiny::textInput( 6 | inputId = ns("name_filter"), 7 | label = "Name" 8 | ), 9 | shiny::selectInput( 10 | inputId = ns("gender_filter"), 11 | label = "Gender", 12 | choices = c("", "female", "male", "genderless", "unknown") 13 | ), 14 | shiny::selectInput( 15 | inputId = ns("status_filter"), 16 | label = "Status", 17 | choices = c("", "alive", "dead", "unknown") 18 | ) 19 | ) 20 | } 21 | 22 | mod_filters_server <- function(id) { 23 | shiny::moduleServer( 24 | id = id, 25 | function(input, output, session) { 26 | filter_settings <- reactive({ 27 | 28 | list( 29 | name = input$name_filter, 30 | status = input$status_filter, 31 | gender = input$gender_filter 32 | ) 33 | }) 34 | 35 | return(filter_settings) 36 | } 37 | ) 38 | } -------------------------------------------------------------------------------- /R/table-mod_pagination_controls.R: -------------------------------------------------------------------------------- 1 | mod_pagination_controls_ui <- function(id) { 2 | ns <- shiny::NS(id) 3 | shiny::div( 4 | class = "pagination-container", 5 | shiny::span(id = ns("pages_text"), class = "pages-text"), 6 | shiny::actionButton( 7 | inputId = ns("goto_first_page"), 8 | class = "pagination-control", 9 | label = "", 10 | icon = shiny::icon("angle-double-left") 11 | ), 12 | shiny::actionButton( 13 | inputId = ns("prev_page"), 14 | class = "pagination-control", 15 | label = "", 16 | icon = shiny::icon("angle-left") 17 | ), 18 | shiny::actionButton( 19 | inputId = ns("next_page"), 20 | class = "pagination-control", 21 | label = "", 22 | icon = shiny::icon("angle-right") 23 | ), 24 | shiny::actionButton( 25 | inputId = ns("goto_last_page"), 26 | class = "pagination-control", 27 | label = "", 28 | icon = shiny::icon("angle-double-right") 29 | ) 30 | ) 31 | } 32 | 33 | mod_pagination_controls_server <- function(id, page_number, total_pages) { 34 | shiny::moduleServer( 35 | id = id, 36 | function(input, output, session) { 37 | observeEvent(input$goto_first_page, { 38 | page_number(1) 39 | }) 40 | 41 | observeEvent(input$goto_last_page, { 42 | page_number(total_pages()) 43 | }) 44 | 45 | observeEvent(input$prev_page, { 46 | current_page_number <- page_number() 47 | page_number(current_page_number - 1) 48 | }) 49 | 50 | observeEvent(input$next_page, { 51 | current_page_number <- page_number() 52 | page_number(current_page_number + 1) 53 | }) 54 | 55 | observeEvent(list(page_number(), total_pages()), { 56 | shiny::req(page_number()) 57 | shiny::req(total_pages()) 58 | 59 | left_bound_condition <- page_number() > 1 60 | shinyjs::toggleState(id = "prev_page", condition = left_bound_condition) 61 | shinyjs::toggleState(id = "goto_first_page", condition = left_bound_condition) 62 | 63 | right_bound_condition <- page_number() < total_pages() 64 | shinyjs::toggleState(id = "next_page", condition = right_bound_condition) 65 | shinyjs::toggleState(id = "goto_last_page", condition = right_bound_condition) 66 | }) 67 | 68 | observeEvent(list(page_number(), total_pages()), { 69 | page_number <- page_number() 70 | total_pages <- total_pages() 71 | text <- glue::glue("{page_number} of {total_pages} pages") 72 | shinyjs::html(id = "pages_text", html = text) 73 | }) 74 | } 75 | ) 76 | } -------------------------------------------------------------------------------- /R/table-ui-character_image.R: -------------------------------------------------------------------------------- 1 | create_character_image <- function(image_url) { 2 | shiny::img( 3 | src = image_url, 4 | alt = image_url, 5 | class = "character-image" 6 | ) 7 | } -------------------------------------------------------------------------------- /R/table-ui-gender_info.R: -------------------------------------------------------------------------------- 1 | create_gender_column_content <- function(gender_value) { 2 | icon_name <- switch( 3 | gender_value, 4 | Male = "mars", 5 | Female = "venus", 6 | Genderless = "genderless", 7 | unknown = "question" 8 | ) 9 | 10 | shiny::div( 11 | class = "gender-cell-content", 12 | shiny::icon(icon_name, class = glue::glue_safe("{icon_name}-icon")), 13 | gender_value 14 | ) 15 | } 16 | -------------------------------------------------------------------------------- /R/table-ui-status_badge.R: -------------------------------------------------------------------------------- 1 | create_status_badge <- function(status) { 2 | style_name <- switch( 3 | status, 4 | Alive = "alive", 5 | unknown = "unknown", 6 | Dead = "dead" 7 | ) 8 | 9 | class_name <- glue::glue_safe("status-badge-{style_name}") 10 | 11 | shiny::div( 12 | shiny::span(status, class = class_name) 13 | ) 14 | } 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Fast Big Data Tables in Shiny 2 | 3 | The R package ecosystem offers a wide variety of packages that make visualizing table data in Shiny apps a breeze. However, with increasing amounts of data our app may become slow and in extreme cases crash due to insufficient memory. 4 | 5 | Those issues can be avoided by using **pagination** in external services outside of our app such as **REST APIs** or **databases**. In this tutorial we are going to show you how to build your own table in Shiny that uses this technique in form of an app showcasing data on Rick and Morty characters (data provided by the [Rick and Morty API](https://rickandmortyapi.com/)). 6 | 7 | Additionally, at the end we will spend some time on beautifying our table by leveraging the custom rendering and styling features of [reactable](https://glin.github.io/reactable/), so stay tuned until the end! 8 | 9 | > Explore your favorite Rick and Morty characters by checking out the finished product on [shinyapps.io](https://rszymanski.shinyapps.io/table-contest/) 10 | 11 | > You can find the full source code on [GitHub](https://github.com/rszymanski/table-contest) 12 | 13 | ## What is Pagination? 14 | 15 | You might have already encountered the term **pagination** when working with table packages. Usually it allows you to display a subset of the data (a page) of a fixed size and provides controls for switching between pages. 16 | 17 | The same concept can be applied when retrieving data from REST APIs or databases. Imagine that you are sending a request that would result in a response containing millions of records. Not only will it be slow and take large amounts of your apps memory, but it might even result in a crash of your application! Instead, you can break down all of the records into pages of fixed size, fetch only the first page and display it in the UI. If the user wants to investigate data from other pages, than you can retrieve subsequent pages as needed. 18 | 19 | Usually REST APIs that offer pagination allow you to specify the size of the page and the number of the page that you would like to retrieve. In case of the [Rick and Morty API](https://rickandmortyapi.com/) the responses are limited up to 20 documents by default. The result of an example GET request to the https://rickandmortyapi.com/api/character endpoint is provided below: 20 | 21 | ``` 22 | { 23 | "info": { 24 | "count": 826, 25 | "pages": 42, 26 | "next": https://rickandmortyapi.com/api/character?page=2, 27 | "prev": null 28 | }, 29 | "results": [ 30 | // up to 20 documents 31 | ] 32 | } 33 | ``` 34 | 35 | The result includes information on the amount of all records, number of pages, a url for fetching the next and previous page as well as a results section containing the content of the requested page. 36 | 37 | We can simulate the same behavior when querying data from a database. For example in [SQLite](https://www.sqlite.org/index.html) we can select subsets of our table by using the [LIMIT and OFFSET clauses](https://www.sqlite.org/lang_select.html#the_limit_clause) e.g. `SELECT * FROM LIMIT 10 OFFSET 10;` 38 | 39 | ## Building the App 40 | We will now go through the steps of building a Shiny app with a table that makes use of the pagination offered by the [Rick and Morty API](https://rickandmortyapi.com/). Make sure that you have the following packages installed: 41 | 42 | ```r 43 | library(data.table) 44 | library(glue) 45 | library(httr) 46 | library(reactable) 47 | library(shiny) 48 | ``` 49 | 50 | Let's start by creating a function for retrieving a specific page of characters: 51 | ```r 52 | fetch_page <- function(page_number) { 53 | url <- glue_safe("https://rickandmortyapi.com/api/character?page={page_number}") 54 | api_response <- GET(url = url) 55 | response_content <- content(api_response) 56 | 57 | retrieved_data <- rbindlist( 58 | lapply( 59 | response_content$results, 60 | function(character) { 61 | data.frame( 62 | image = character$image, 63 | name = character$name, 64 | gender = character$gender, 65 | status = character$status, 66 | species = character$species, 67 | type = character$type 68 | ) 69 | } 70 | ) 71 | ) 72 | 73 | list( 74 | total_pages = response_content$info$pages, 75 | data = retrieved_data, 76 | page_count = response_content$info$count, 77 | next_page_url = response_content$info$`next` 78 | ) 79 | } 80 | ``` 81 | 82 | The UI of the app will consist of two buttons for navigating between previous and next pages and it will display the retrieved data in a table as is. 83 | 84 | ```r 85 | ui <- fluidPage( 86 | actionButton("prev_page", label = "", icon = icon("angle-left")), 87 | actionButton("next_page", label = "", icon = icon("angle-right")), 88 | reactableOutput(outputId = "data_table") 89 | ) 90 | ``` 91 | 92 | On the server part, we are going to keep track of the selected page and fetch a subset of the data whenever it changes. 93 | 94 | ```r 95 | server <- function(input, output, session) { 96 | page_number <- reactiveVal(1) 97 | 98 | observeEvent(input$prev_page, { 99 | current_page_number <- page_number() 100 | page_number(current_page_number - 1) 101 | }) 102 | 103 | observeEvent(input$next_page, { 104 | current_page_number <- page_number() 105 | page_number(current_page_number + 1) 106 | }) 107 | 108 | current_data <- reactive({ 109 | req(page_number()) 110 | fetch_page(page_number = page_number()) 111 | }) 112 | 113 | output$data_table <- renderReactable({ 114 | req(current_data()) 115 | req(nrow(current_data()$data) > 0) 116 | 117 | table_data <- current_data()$data 118 | 119 | reactable( 120 | data = table_data, 121 | sortable = FALSE, 122 | pagination = FALSE 123 | ) 124 | }) 125 | } 126 | ``` 127 | 128 | Let's check out our app by running: 129 | ```r 130 | shinyApp(ui, server) 131 | ``` 132 | 133 | ![](./gifs/raw-shiny-table.gif) 134 | 135 | You might wonder, whether all of that work was worth the effort. Especially in case of this example as the data source has a total of 826 characters that would easily fit into the apps memory. However, even in such a small example we can see the benefits of using pagination. Let's compare an app that uses REST API pagination to another version that fetches all of the data page by page into memory upfront and then displays it: 136 | 137 | Pagination | Fetching all data upfront 138 | :---------------------:|:-------------------------: 139 | ![](./gifs/pagination-case.gif) | ![](./gifs/no-pagination-case.gif) 140 | 141 | As you can see, the version fetching all of data upfront takes a couple of seconds before displaying the data while the version that uses pagination shows the data instantly. Moreover, the paginated version of the app takes up less memory as it only keeps one page of data in memory at a time. The benefits of this approach in terms of speed and memory usage will increase the larger our data becomes. 142 | 143 | ## Making our table more visually appealing 144 | We managed to fix our problem with handling large size data in our table visualizations. However, the result is looking a bit raw and asks for improvements. The `reactable` package provides an interface for applying custom styling and rendering to table elements. We will leverage those features to make our table more visually appealing. 145 | 146 | ### Displaying Images Instead of Links 147 | The image field in our response contains a link to a `.jpeg` image depicting the character from the show. Instead of displaying the link, we can display the actual image. This can be achieved by defining a custom render for the `image` column. 148 | 149 | ```r 150 | reactable( 151 | data = table_data, 152 | sortable = FALSE, 153 | pagination = FALSE, 154 | columns = list( 155 | image = colDef( 156 | name = "", 157 | maxWidth = 88, 158 | cell = function(value) { 159 | img(src = value, alt = value, style = list(height = "80px", `border-radius` = "50%")) 160 | } 161 | ) 162 | ) 163 | ) 164 | ``` 165 | 166 |

167 | 168 |

169 | 170 | ### Introducing Visual Hierarchy 171 | Our table already looks a lot better with the added image. However, all of our remaining column values currently attract the same amount of attention. Let's start by emphasizing the `name` values and de-emphasizing the `species` and `type` columns. We will make use of font weights, font sizes and color to introduce a hierarchy for those values: 172 | 173 | ```r 174 | reactable( 175 | data = table_data, 176 | sortable = FALSE, 177 | pagination = FALSE, 178 | columns = list( 179 | image = // image creation function 180 | name = colDef( 181 | name = "Name", 182 | cell = function(value, row_index) { 183 | div( 184 | div( 185 | style = list(`font-weight` = "700", `font-size` = "16px"), 186 | value 187 | ), 188 | div( 189 | style = list(color = "hsl(201, 23%, 34%)", `font-size` = "16px"), 190 | table_data[row_index, ]$species 191 | ), 192 | div( 193 | style = list(color = "hsl(203, 15%, 47%)", `font-size` = "14px"), 194 | table_data[row_index, ]$type 195 | ) 196 | ) 197 | } 198 | ), 199 | type = colDef(show = FALSE), 200 | species = colDef(show = FALSE) 201 | ) 202 | ) 203 | ``` 204 | 205 |

206 | 207 |

208 | 209 | ### Enriching Data with Color 210 | One way of enriching table data can be achieved by introducing color. Let's make use of that in the `status` column by turning the plain text values into status pills. 211 | 212 | ```r 213 | reactable( 214 | data = table_data, 215 | sortable = FALSE, 216 | pagination = FALSE, 217 | columns = list( 218 | image = ..., // create image column 219 | name = ... , // create name column 220 | type = colDef(show = FALSE), 221 | species = colDef(show = FALSE), 222 | status = colDef( 223 | name = "Status", 224 | cell = function(value) { 225 | styles <- list( 226 | `border-radius` = "16px", 227 | padding = "4px 12px" 228 | ) 229 | 230 | background_color_value <- switch( 231 | value, 232 | Alive = "hsl(116, 60%, 90%)", 233 | unknown = "hsl(230, 70%, 90%)", 234 | Dead = "hsl(350, 70%, 90%)" 235 | ) 236 | 237 | color_value <- switch( 238 | value, 239 | Alive = "hsl(116, 30%, 25%)", 240 | unknown = "hsl(230, 45%, 30%)", 241 | Dead = "hsl(350, 45%, 30%)" 242 | ) 243 | 244 | styles$color <- color_value 245 | styles$`background-color` <- background_color_value 246 | 247 | 248 | div( 249 | span(value, style = styles) 250 | ) 251 | } 252 | ) 253 | ) 254 | ) 255 | ``` 256 | 257 |

258 | 259 |

260 | 261 | ### Enriching Data with Icons 262 | Another way of enriching the table data can be accomplished by adding icons beside the plain text values. Let's add some icons to the `gender` column. 263 | 264 | ```r 265 | reactable( 266 | data = table_data, 267 | sortable = FALSE, 268 | pagination = FALSE, 269 | columns = list( 270 | image = ..., // create image column 271 | name = ... , // create name column 272 | type = colDef(show = FALSE), 273 | species = colDef(show = FALSE), 274 | status = ..., // create status column, 275 | gender = colDef( 276 | name = "Gender", 277 | cell = function(value) { 278 | styles <- list(`margin-right` = "8px") 279 | 280 | icon_name <- switch( 281 | value, 282 | Male = "mars", 283 | Female = "venus", 284 | Genderless = "genderless", 285 | unknown = "question" 286 | ) 287 | 288 | icon_color <- switch( 289 | value, 290 | Male = "lightblue", 291 | Female = "pink", 292 | Genderless = "gray", 293 | unknown = "hsl(31, 100%, 70%)" 294 | ) 295 | 296 | styles$color <- icon_color 297 | 298 | div( 299 | icon(icon_name, style = styles), 300 | value 301 | ) 302 | } 303 | ) 304 | ) 305 | ) 306 | ``` 307 | 308 |

309 | 310 |

311 | 312 | 313 | ### Finishing Touches 314 | We already managed to improve the look of our table, however there are still some minor adjustments needed. For example, have a look at the status values, the outline of the pill is very close to the top row separator. Another thing that needs improvements are the column header values. Currently they are bold and divert attention from the character names. That can be fixed by centering the cell content vertically and making the header column names lighter. 315 | 316 | ```r 317 | reactable::reactable( 318 | data = table_data, 319 | sortable = FALSE, 320 | pagination = FALSE, 321 | columns = ... // create columns 322 | theme = reactable::reactableTheme( 323 | cellStyle = list( 324 | display = "flex", 325 | flexDirection = "column", 326 | justifyContent = "center" 327 | ) 328 | ), 329 | defaultColDef = reactable::colDef( 330 | headerStyle = list(`text-transform` = "uppercase",color = "hsl(203, 15%, 47%)") 331 | ) 332 | ) 333 | ``` 334 | 335 |

336 | 337 |

338 | 339 | Now, let's compare our result with the table that we initially started working with. You can be amazed at how much of a difference a couple of visual enhancements make on the overall look. 340 | 341 | Before styling | After styling 342 | :---------------------:|:-------------------------: 343 | ![](./gifs/table-raw.png) | ![](./gifs/added-finishing-touches.png) 344 | -------------------------------------------------------------------------------- /app.R: -------------------------------------------------------------------------------- 1 | library(shiny) 2 | library(sass) 3 | library(reactable) 4 | library(magrittr) 5 | 6 | sass( 7 | sass_file("styles/main.scss"), 8 | output = "www/main.css" 9 | ) 10 | 11 | CONFIG <- config::get() 12 | 13 | ui <- shiny::fluidPage( 14 | shinyjs::useShinyjs(), 15 | shiny::tags$head( 16 | shiny::tags$link(href = "main.css", rel = "stylesheet", type = "text/css") 17 | ), 18 | title = "Rick and Morty Characters Explorer", 19 | shiny::div( 20 | style = "display: flex; flex-direction: column; align-items: center;", 21 | shiny::div( 22 | style = "width: 984px; padding: 12px", 23 | mod_filters_ui(id = "character_table_filters"), 24 | shiny::div( 25 | class = "round-box", 26 | shinyjs::hidden( 27 | shiny::div( 28 | id = "table_placeholder", 29 | class = "table-placeholder", 30 | shiny::icon("question-circle", class = "placeholder-icon"), 31 | shiny::div( 32 | "Oops, looks like there is no character matching your search criteria", 33 | class = "placeholder-text" 34 | ) 35 | ) 36 | ), 37 | shiny::div( 38 | id = "table_container", 39 | mod_pagination_controls_ui("chracter_table_pagination_controls"), 40 | mod_characters_table_ui(id = "characters_table", width = "900px", height = "70vh") %>% 41 | shinycssloaders::withSpinner(8) 42 | ) 43 | ) 44 | ) 45 | ) 46 | ) 47 | 48 | server <- function(input, output, session) { 49 | character_data_fetcher <- CharacterDataFetcherFactory$new( 50 | api_url = "https://rickandmortyapi.com/api/character" 51 | )$create(CONFIG$data_fetcher_type) 52 | 53 | page_number <- shiny::reactiveVal(1) 54 | 55 | filter_settings <- mod_filters_server(id = "character_table_filters") 56 | 57 | 58 | observeEvent(current_data(), { 59 | shiny::req(current_data()) 60 | 61 | should_table_be_showed <- nrow(current_data()$data) > 0 62 | shinyjs::toggle(id = "table_placeholder", condition = !should_table_be_showed) 63 | shinyjs::toggle(id = "table_container", condition = should_table_be_showed) 64 | }) 65 | 66 | observeEvent(filter_settings(), { 67 | page_number(1) 68 | }) 69 | 70 | current_data <- reactive({ 71 | shiny::req(page_number()) 72 | shiny::req(filter_settings()) 73 | 74 | character_data_fetcher$fetch_page( 75 | page_number = page_number(), 76 | filter_settings = filter_settings() 77 | ) 78 | }) 79 | 80 | total_pages <- reactive({ 81 | shiny::req(current_data) 82 | current_data()$total_pages 83 | }) 84 | 85 | mod_characters_table_server(id = "characters_table", current_data = current_data) 86 | mod_pagination_controls_server(id = "chracter_table_pagination_controls", page_number = page_number, total_pages = total_pages) 87 | } 88 | 89 | shinyApp(ui, server) 90 | -------------------------------------------------------------------------------- /config.yml: -------------------------------------------------------------------------------- 1 | default: 2 | data_fetcher_type: "paginated" 3 | 4 | no_pagination: 5 | data_fetcher_type: "in_memory" -------------------------------------------------------------------------------- /gifs/added-character-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rszymanski/table-contest/71d53fae560e872337ac733b6496c8f35616433f/gifs/added-character-image.png -------------------------------------------------------------------------------- /gifs/added-colors.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rszymanski/table-contest/71d53fae560e872337ac733b6496c8f35616433f/gifs/added-colors.png -------------------------------------------------------------------------------- /gifs/added-finishing-touches.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rszymanski/table-contest/71d53fae560e872337ac733b6496c8f35616433f/gifs/added-finishing-touches.png -------------------------------------------------------------------------------- /gifs/added-hierarchy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rszymanski/table-contest/71d53fae560e872337ac733b6496c8f35616433f/gifs/added-hierarchy.png -------------------------------------------------------------------------------- /gifs/added-icons.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rszymanski/table-contest/71d53fae560e872337ac733b6496c8f35616433f/gifs/added-icons.png -------------------------------------------------------------------------------- /gifs/app-showcase.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rszymanski/table-contest/71d53fae560e872337ac733b6496c8f35616433f/gifs/app-showcase.gif -------------------------------------------------------------------------------- /gifs/final-table.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rszymanski/table-contest/71d53fae560e872337ac733b6496c8f35616433f/gifs/final-table.png -------------------------------------------------------------------------------- /gifs/no-pagination-case.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rszymanski/table-contest/71d53fae560e872337ac733b6496c8f35616433f/gifs/no-pagination-case.gif -------------------------------------------------------------------------------- /gifs/pagination-case.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rszymanski/table-contest/71d53fae560e872337ac733b6496c8f35616433f/gifs/pagination-case.gif -------------------------------------------------------------------------------- /gifs/raw-shiny-table.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rszymanski/table-contest/71d53fae560e872337ac733b6496c8f35616433f/gifs/raw-shiny-table.gif -------------------------------------------------------------------------------- /gifs/table-raw.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rszymanski/table-contest/71d53fae560e872337ac733b6496c8f35616433f/gifs/table-raw.png -------------------------------------------------------------------------------- /renv.lock: -------------------------------------------------------------------------------- 1 | { 2 | "R": { 3 | "Version": "4.0.5", 4 | "Repositories": [ 5 | { 6 | "Name": "CRAN", 7 | "URL": "https://cran.rstudio.com" 8 | } 9 | ] 10 | }, 11 | "Packages": { 12 | "R6": { 13 | "Package": "R6", 14 | "Version": "2.5.0", 15 | "Source": "Repository", 16 | "Repository": "CRAN", 17 | "Hash": "b203113193e70978a696b2809525649d" 18 | }, 19 | "Rcpp": { 20 | "Package": "Rcpp", 21 | "Version": "1.0.6", 22 | "Source": "Repository", 23 | "Repository": "CRAN", 24 | "Hash": "dbb5e436998a7eba5a9d682060533338" 25 | }, 26 | "askpass": { 27 | "Package": "askpass", 28 | "Version": "1.1", 29 | "Source": "Repository", 30 | "Repository": "CRAN", 31 | "Hash": "e8a22846fff485f0be3770c2da758713" 32 | }, 33 | "base64enc": { 34 | "Package": "base64enc", 35 | "Version": "0.1-3", 36 | "Source": "Repository", 37 | "Repository": "CRAN", 38 | "Hash": "543776ae6848fde2f48ff3816d0628bc" 39 | }, 40 | "bslib": { 41 | "Package": "bslib", 42 | "Version": "0.2.5", 43 | "Source": "Repository", 44 | "Repository": "CRAN", 45 | "Hash": "e2049406d0e4f37de7b9f1a1a794eb8a" 46 | }, 47 | "cachem": { 48 | "Package": "cachem", 49 | "Version": "1.0.5", 50 | "Source": "Repository", 51 | "Repository": "CRAN", 52 | "Hash": "5346f76a33eb7417812c270b04a5581b" 53 | }, 54 | "commonmark": { 55 | "Package": "commonmark", 56 | "Version": "1.7", 57 | "Source": "Repository", 58 | "Repository": "CRAN", 59 | "Hash": "0f22be39ec1d141fd03683c06f3a6e67" 60 | }, 61 | "config": { 62 | "Package": "config", 63 | "Version": "0.3.1", 64 | "Source": "Repository", 65 | "Repository": "CRAN", 66 | "Hash": "31d77b09f63550cee9ecb5a08bf76e8f" 67 | }, 68 | "crayon": { 69 | "Package": "crayon", 70 | "Version": "1.4.1", 71 | "Source": "Repository", 72 | "Repository": "CRAN", 73 | "Hash": "e75525c55c70e5f4f78c9960a4b402e9" 74 | }, 75 | "curl": { 76 | "Package": "curl", 77 | "Version": "4.3.1", 78 | "Source": "Repository", 79 | "Repository": "CRAN", 80 | "Hash": "c5e68405893f030f139f6d6675eac675" 81 | }, 82 | "data.table": { 83 | "Package": "data.table", 84 | "Version": "1.14.0", 85 | "Source": "Repository", 86 | "Repository": "CRAN", 87 | "Hash": "d1b8b1a821ee564a3515fa6c6d5c52dc" 88 | }, 89 | "digest": { 90 | "Package": "digest", 91 | "Version": "0.6.27", 92 | "Source": "Repository", 93 | "Repository": "CRAN", 94 | "Hash": "a0cbe758a531d054b537d16dff4d58a1" 95 | }, 96 | "ellipsis": { 97 | "Package": "ellipsis", 98 | "Version": "0.3.2", 99 | "Source": "Repository", 100 | "Repository": "CRAN", 101 | "Hash": "bb0eec2fe32e88d9e2836c2f73ea2077" 102 | }, 103 | "fastmap": { 104 | "Package": "fastmap", 105 | "Version": "1.1.0", 106 | "Source": "Repository", 107 | "Repository": "CRAN", 108 | "Hash": "77bd60a6157420d4ffa93b27cf6a58b8" 109 | }, 110 | "fs": { 111 | "Package": "fs", 112 | "Version": "1.5.0", 113 | "Source": "Repository", 114 | "Repository": "CRAN", 115 | "Hash": "44594a07a42e5f91fac9f93fda6d0109" 116 | }, 117 | "glue": { 118 | "Package": "glue", 119 | "Version": "1.4.2", 120 | "Source": "Repository", 121 | "Repository": "CRAN", 122 | "Hash": "6efd734b14c6471cfe443345f3e35e29" 123 | }, 124 | "htmltools": { 125 | "Package": "htmltools", 126 | "Version": "0.5.1.1", 127 | "Source": "Repository", 128 | "Repository": "CRAN", 129 | "Hash": "af2c2531e55df5cf230c4b5444fc973c" 130 | }, 131 | "htmlwidgets": { 132 | "Package": "htmlwidgets", 133 | "Version": "1.5.3", 134 | "Source": "Repository", 135 | "Repository": "CRAN", 136 | "Hash": "6fdaa86d0700f8b3e92ee3c445a5a10d" 137 | }, 138 | "httpuv": { 139 | "Package": "httpuv", 140 | "Version": "1.6.1", 141 | "Source": "Repository", 142 | "Repository": "CRAN", 143 | "Hash": "54344a78aae37bc6ef39b1240969df8e" 144 | }, 145 | "httr": { 146 | "Package": "httr", 147 | "Version": "1.4.2", 148 | "Source": "Repository", 149 | "Repository": "CRAN", 150 | "Hash": "a525aba14184fec243f9eaec62fbed43" 151 | }, 152 | "jquerylib": { 153 | "Package": "jquerylib", 154 | "Version": "0.1.4", 155 | "Source": "Repository", 156 | "Repository": "CRAN", 157 | "Hash": "5aab57a3bd297eee1c1d862735972182" 158 | }, 159 | "jsonlite": { 160 | "Package": "jsonlite", 161 | "Version": "1.7.2", 162 | "Source": "Repository", 163 | "Repository": "CRAN", 164 | "Hash": "98138e0994d41508c7a6b84a0600cfcb" 165 | }, 166 | "later": { 167 | "Package": "later", 168 | "Version": "1.2.0", 169 | "Source": "Repository", 170 | "Repository": "CRAN", 171 | "Hash": "b61890ae77fea19fc8acadd25db70aa4" 172 | }, 173 | "lifecycle": { 174 | "Package": "lifecycle", 175 | "Version": "1.0.0", 176 | "Source": "Repository", 177 | "Repository": "CRAN", 178 | "Hash": "3471fb65971f1a7b2d4ae7848cf2db8d" 179 | }, 180 | "magrittr": { 181 | "Package": "magrittr", 182 | "Version": "2.0.1", 183 | "Source": "Repository", 184 | "Repository": "CRAN", 185 | "Hash": "41287f1ac7d28a92f0a286ed507928d3" 186 | }, 187 | "mime": { 188 | "Package": "mime", 189 | "Version": "0.10", 190 | "Source": "Repository", 191 | "Repository": "CRAN", 192 | "Hash": "26fa77e707223e1ce042b2b5d09993dc" 193 | }, 194 | "openssl": { 195 | "Package": "openssl", 196 | "Version": "1.4.4", 197 | "Source": "Repository", 198 | "Repository": "CRAN", 199 | "Hash": "f4dbc5a47fd93d3415249884d31d6791" 200 | }, 201 | "promises": { 202 | "Package": "promises", 203 | "Version": "1.2.0.1", 204 | "Source": "Repository", 205 | "Repository": "CRAN", 206 | "Hash": "4ab2c43adb4d4699cf3690acd378d75d" 207 | }, 208 | "rappdirs": { 209 | "Package": "rappdirs", 210 | "Version": "0.3.3", 211 | "Source": "Repository", 212 | "Repository": "CRAN", 213 | "Hash": "5e3c5dc0b071b21fa128676560dbe94d" 214 | }, 215 | "reactR": { 216 | "Package": "reactR", 217 | "Version": "0.4.4", 218 | "Source": "Repository", 219 | "Repository": "CRAN", 220 | "Hash": "75389c8091eb14ee21c6bc87a88b3809" 221 | }, 222 | "reactable": { 223 | "Package": "reactable", 224 | "Version": "0.2.3", 225 | "Source": "Repository", 226 | "Repository": "CRAN", 227 | "Hash": "ac1afe50d1c77470a72971a07fd146b1" 228 | }, 229 | "renv": { 230 | "Package": "renv", 231 | "Version": "0.13.2", 232 | "Source": "Repository", 233 | "Repository": "CRAN", 234 | "Hash": "079cb1f03ff972b30401ed05623cbe92" 235 | }, 236 | "rlang": { 237 | "Package": "rlang", 238 | "Version": "0.4.11", 239 | "Source": "Repository", 240 | "Repository": "CRAN", 241 | "Hash": "515f341d3affe0de9e4a7f762efb0456" 242 | }, 243 | "sass": { 244 | "Package": "sass", 245 | "Version": "0.4.0", 246 | "Source": "Repository", 247 | "Repository": "CRAN", 248 | "Hash": "50cf822feb64bb3977bda0b7091be623" 249 | }, 250 | "shiny": { 251 | "Package": "shiny", 252 | "Version": "1.6.0", 253 | "Source": "Repository", 254 | "Repository": "CRAN", 255 | "Hash": "6e3b6ae7fe02b5859e4bb277f218b8ae" 256 | }, 257 | "shinycssloaders": { 258 | "Package": "shinycssloaders", 259 | "Version": "1.0.0", 260 | "Source": "Repository", 261 | "Repository": "CRAN", 262 | "Hash": "f39bb3c44a9b496723ec7e86f9a771d8" 263 | }, 264 | "shinyjs": { 265 | "Package": "shinyjs", 266 | "Version": "2.0.0", 267 | "Source": "Repository", 268 | "Repository": "CRAN", 269 | "Hash": "9ddfc91d4280eaa34c2103951538976f" 270 | }, 271 | "sourcetools": { 272 | "Package": "sourcetools", 273 | "Version": "0.1.7", 274 | "Source": "Repository", 275 | "Repository": "CRAN", 276 | "Hash": "947e4e02a79effa5d512473e10f41797" 277 | }, 278 | "sys": { 279 | "Package": "sys", 280 | "Version": "3.4", 281 | "Source": "Repository", 282 | "Repository": "CRAN", 283 | "Hash": "b227d13e29222b4574486cfcbde077fa" 284 | }, 285 | "withr": { 286 | "Package": "withr", 287 | "Version": "2.4.2", 288 | "Source": "Repository", 289 | "Repository": "CRAN", 290 | "Hash": "ad03909b44677f930fa156d47d7a3aeb" 291 | }, 292 | "xtable": { 293 | "Package": "xtable", 294 | "Version": "1.8-4", 295 | "Source": "Repository", 296 | "Repository": "CRAN", 297 | "Hash": "b8acdf8af494d9ec19ccb2481a9b11c2" 298 | }, 299 | "yaml": { 300 | "Package": "yaml", 301 | "Version": "2.2.1", 302 | "Source": "Repository", 303 | "Repository": "CRAN", 304 | "Hash": "2826c5d9efb0a88f657c7a679c7106db" 305 | } 306 | } 307 | } 308 | -------------------------------------------------------------------------------- /renv/.gitignore: -------------------------------------------------------------------------------- 1 | library/ 2 | local/ 3 | lock/ 4 | python/ 5 | staging/ 6 | -------------------------------------------------------------------------------- /renv/activate.R: -------------------------------------------------------------------------------- 1 | 2 | local({ 3 | 4 | # the requested version of renv 5 | version <- "0.13.2" 6 | 7 | # the project directory 8 | project <- getwd() 9 | 10 | # avoid recursion 11 | if (!is.na(Sys.getenv("RENV_R_INITIALIZING", unset = NA))) 12 | return(invisible(TRUE)) 13 | 14 | # signal that we're loading renv during R startup 15 | Sys.setenv("RENV_R_INITIALIZING" = "true") 16 | on.exit(Sys.unsetenv("RENV_R_INITIALIZING"), add = TRUE) 17 | 18 | # signal that we've consented to use renv 19 | options(renv.consent = TRUE) 20 | 21 | # load the 'utils' package eagerly -- this ensures that renv shims, which 22 | # mask 'utils' packages, will come first on the search path 23 | library(utils, lib.loc = .Library) 24 | 25 | # check to see if renv has already been loaded 26 | if ("renv" %in% loadedNamespaces()) { 27 | 28 | # if renv has already been loaded, and it's the requested version of renv, 29 | # nothing to do 30 | spec <- .getNamespaceInfo(.getNamespace("renv"), "spec") 31 | if (identical(spec[["version"]], version)) 32 | return(invisible(TRUE)) 33 | 34 | # otherwise, unload and attempt to load the correct version of renv 35 | unloadNamespace("renv") 36 | 37 | } 38 | 39 | # load bootstrap tools 40 | bootstrap <- function(version, library) { 41 | 42 | # attempt to download renv 43 | tarball <- tryCatch(renv_bootstrap_download(version), error = identity) 44 | if (inherits(tarball, "error")) 45 | stop("failed to download renv ", version) 46 | 47 | # now attempt to install 48 | status <- tryCatch(renv_bootstrap_install(version, tarball, library), error = identity) 49 | if (inherits(status, "error")) 50 | stop("failed to install renv ", version) 51 | 52 | } 53 | 54 | renv_bootstrap_tests_running <- function() { 55 | getOption("renv.tests.running", default = FALSE) 56 | } 57 | 58 | renv_bootstrap_repos <- function() { 59 | 60 | # check for repos override 61 | repos <- Sys.getenv("RENV_CONFIG_REPOS_OVERRIDE", unset = NA) 62 | if (!is.na(repos)) 63 | return(repos) 64 | 65 | # if we're testing, re-use the test repositories 66 | if (renv_bootstrap_tests_running()) 67 | return(getOption("renv.tests.repos")) 68 | 69 | # retrieve current repos 70 | repos <- getOption("repos") 71 | 72 | # ensure @CRAN@ entries are resolved 73 | repos[repos == "@CRAN@"] <- getOption( 74 | "renv.repos.cran", 75 | "https://cloud.r-project.org" 76 | ) 77 | 78 | # add in renv.bootstrap.repos if set 79 | default <- c(FALLBACK = "https://cloud.r-project.org") 80 | extra <- getOption("renv.bootstrap.repos", default = default) 81 | repos <- c(repos, extra) 82 | 83 | # remove duplicates that might've snuck in 84 | dupes <- duplicated(repos) | duplicated(names(repos)) 85 | repos[!dupes] 86 | 87 | } 88 | 89 | renv_bootstrap_download <- function(version) { 90 | 91 | # if the renv version number has 4 components, assume it must 92 | # be retrieved via github 93 | nv <- numeric_version(version) 94 | components <- unclass(nv)[[1]] 95 | 96 | methods <- if (length(components) == 4L) { 97 | list( 98 | renv_bootstrap_download_github 99 | ) 100 | } else { 101 | list( 102 | renv_bootstrap_download_cran_latest, 103 | renv_bootstrap_download_cran_archive 104 | ) 105 | } 106 | 107 | for (method in methods) { 108 | path <- tryCatch(method(version), error = identity) 109 | if (is.character(path) && file.exists(path)) 110 | return(path) 111 | } 112 | 113 | stop("failed to download renv ", version) 114 | 115 | } 116 | 117 | renv_bootstrap_download_impl <- function(url, destfile) { 118 | 119 | mode <- "wb" 120 | 121 | # https://bugs.r-project.org/bugzilla/show_bug.cgi?id=17715 122 | fixup <- 123 | Sys.info()[["sysname"]] == "Windows" && 124 | substring(url, 1L, 5L) == "file:" 125 | 126 | if (fixup) 127 | mode <- "w+b" 128 | 129 | utils::download.file( 130 | url = url, 131 | destfile = destfile, 132 | mode = mode, 133 | quiet = TRUE 134 | ) 135 | 136 | } 137 | 138 | renv_bootstrap_download_cran_latest <- function(version) { 139 | 140 | spec <- renv_bootstrap_download_cran_latest_find(version) 141 | 142 | message("* Downloading renv ", version, " ... ", appendLF = FALSE) 143 | 144 | type <- spec$type 145 | repos <- spec$repos 146 | 147 | info <- tryCatch( 148 | utils::download.packages( 149 | pkgs = "renv", 150 | destdir = tempdir(), 151 | repos = repos, 152 | type = type, 153 | quiet = TRUE 154 | ), 155 | condition = identity 156 | ) 157 | 158 | if (inherits(info, "condition")) { 159 | message("FAILED") 160 | return(FALSE) 161 | } 162 | 163 | # report success and return 164 | message("OK (downloaded ", type, ")") 165 | info[1, 2] 166 | 167 | } 168 | 169 | renv_bootstrap_download_cran_latest_find <- function(version) { 170 | 171 | # check whether binaries are supported on this system 172 | binary <- 173 | getOption("renv.bootstrap.binary", default = TRUE) && 174 | !identical(.Platform$pkgType, "source") && 175 | !identical(getOption("pkgType"), "source") && 176 | Sys.info()[["sysname"]] %in% c("Darwin", "Windows") 177 | 178 | types <- c(if (binary) "binary", "source") 179 | 180 | # iterate over types + repositories 181 | for (type in types) { 182 | for (repos in renv_bootstrap_repos()) { 183 | 184 | # retrieve package database 185 | db <- tryCatch( 186 | as.data.frame( 187 | utils::available.packages(type = type, repos = repos), 188 | stringsAsFactors = FALSE 189 | ), 190 | error = identity 191 | ) 192 | 193 | if (inherits(db, "error")) 194 | next 195 | 196 | # check for compatible entry 197 | entry <- db[db$Package %in% "renv" & db$Version %in% version, ] 198 | if (nrow(entry) == 0) 199 | next 200 | 201 | # found it; return spec to caller 202 | spec <- list(entry = entry, type = type, repos = repos) 203 | return(spec) 204 | 205 | } 206 | } 207 | 208 | # if we got here, we failed to find renv 209 | fmt <- "renv %s is not available from your declared package repositories" 210 | stop(sprintf(fmt, version)) 211 | 212 | } 213 | 214 | renv_bootstrap_download_cran_archive <- function(version) { 215 | 216 | name <- sprintf("renv_%s.tar.gz", version) 217 | repos <- renv_bootstrap_repos() 218 | urls <- file.path(repos, "src/contrib/Archive/renv", name) 219 | destfile <- file.path(tempdir(), name) 220 | 221 | message("* Downloading renv ", version, " ... ", appendLF = FALSE) 222 | 223 | for (url in urls) { 224 | 225 | status <- tryCatch( 226 | renv_bootstrap_download_impl(url, destfile), 227 | condition = identity 228 | ) 229 | 230 | if (identical(status, 0L)) { 231 | message("OK") 232 | return(destfile) 233 | } 234 | 235 | } 236 | 237 | message("FAILED") 238 | return(FALSE) 239 | 240 | } 241 | 242 | renv_bootstrap_download_github <- function(version) { 243 | 244 | enabled <- Sys.getenv("RENV_BOOTSTRAP_FROM_GITHUB", unset = "TRUE") 245 | if (!identical(enabled, "TRUE")) 246 | return(FALSE) 247 | 248 | # prepare download options 249 | pat <- Sys.getenv("GITHUB_PAT") 250 | if (nzchar(Sys.which("curl")) && nzchar(pat)) { 251 | fmt <- "--location --fail --header \"Authorization: token %s\"" 252 | extra <- sprintf(fmt, pat) 253 | saved <- options("download.file.method", "download.file.extra") 254 | options(download.file.method = "curl", download.file.extra = extra) 255 | on.exit(do.call(base::options, saved), add = TRUE) 256 | } else if (nzchar(Sys.which("wget")) && nzchar(pat)) { 257 | fmt <- "--header=\"Authorization: token %s\"" 258 | extra <- sprintf(fmt, pat) 259 | saved <- options("download.file.method", "download.file.extra") 260 | options(download.file.method = "wget", download.file.extra = extra) 261 | on.exit(do.call(base::options, saved), add = TRUE) 262 | } 263 | 264 | message("* Downloading renv ", version, " from GitHub ... ", appendLF = FALSE) 265 | 266 | url <- file.path("https://api.github.com/repos/rstudio/renv/tarball", version) 267 | name <- sprintf("renv_%s.tar.gz", version) 268 | destfile <- file.path(tempdir(), name) 269 | 270 | status <- tryCatch( 271 | renv_bootstrap_download_impl(url, destfile), 272 | condition = identity 273 | ) 274 | 275 | if (!identical(status, 0L)) { 276 | message("FAILED") 277 | return(FALSE) 278 | } 279 | 280 | message("OK") 281 | return(destfile) 282 | 283 | } 284 | 285 | renv_bootstrap_install <- function(version, tarball, library) { 286 | 287 | # attempt to install it into project library 288 | message("* Installing renv ", version, " ... ", appendLF = FALSE) 289 | dir.create(library, showWarnings = FALSE, recursive = TRUE) 290 | 291 | # invoke using system2 so we can capture and report output 292 | bin <- R.home("bin") 293 | exe <- if (Sys.info()[["sysname"]] == "Windows") "R.exe" else "R" 294 | r <- file.path(bin, exe) 295 | args <- c("--vanilla", "CMD", "INSTALL", "-l", shQuote(library), shQuote(tarball)) 296 | output <- system2(r, args, stdout = TRUE, stderr = TRUE) 297 | message("Done!") 298 | 299 | # check for successful install 300 | status <- attr(output, "status") 301 | if (is.numeric(status) && !identical(status, 0L)) { 302 | header <- "Error installing renv:" 303 | lines <- paste(rep.int("=", nchar(header)), collapse = "") 304 | text <- c(header, lines, output) 305 | writeLines(text, con = stderr()) 306 | } 307 | 308 | status 309 | 310 | } 311 | 312 | renv_bootstrap_platform_prefix <- function() { 313 | 314 | # construct version prefix 315 | version <- paste(R.version$major, R.version$minor, sep = ".") 316 | prefix <- paste("R", numeric_version(version)[1, 1:2], sep = "-") 317 | 318 | # include SVN revision for development versions of R 319 | # (to avoid sharing platform-specific artefacts with released versions of R) 320 | devel <- 321 | identical(R.version[["status"]], "Under development (unstable)") || 322 | identical(R.version[["nickname"]], "Unsuffered Consequences") 323 | 324 | if (devel) 325 | prefix <- paste(prefix, R.version[["svn rev"]], sep = "-r") 326 | 327 | # build list of path components 328 | components <- c(prefix, R.version$platform) 329 | 330 | # include prefix if provided by user 331 | prefix <- renv_bootstrap_platform_prefix_impl() 332 | if (!is.na(prefix) && nzchar(prefix)) 333 | components <- c(prefix, components) 334 | 335 | # build prefix 336 | paste(components, collapse = "/") 337 | 338 | } 339 | 340 | renv_bootstrap_platform_prefix_impl <- function() { 341 | 342 | # if an explicit prefix has been supplied, use it 343 | prefix <- Sys.getenv("RENV_PATHS_PREFIX", unset = NA) 344 | if (!is.na(prefix)) 345 | return(prefix) 346 | 347 | # if the user has requested an automatic prefix, generate it 348 | auto <- Sys.getenv("RENV_PATHS_PREFIX_AUTO", unset = NA) 349 | if (auto %in% c("TRUE", "True", "true", "1")) 350 | return(renv_bootstrap_platform_prefix_auto()) 351 | 352 | # empty string on failure 353 | "" 354 | 355 | } 356 | 357 | renv_bootstrap_platform_prefix_auto <- function() { 358 | 359 | prefix <- tryCatch(renv_bootstrap_platform_os(), error = identity) 360 | if (inherits(prefix, "error") || prefix %in% "unknown") { 361 | 362 | msg <- paste( 363 | "failed to infer current operating system", 364 | "please file a bug report at https://github.com/rstudio/renv/issues", 365 | sep = "; " 366 | ) 367 | 368 | warning(msg) 369 | 370 | } 371 | 372 | prefix 373 | 374 | } 375 | 376 | renv_bootstrap_platform_os <- function() { 377 | 378 | sysinfo <- Sys.info() 379 | sysname <- sysinfo[["sysname"]] 380 | 381 | # handle Windows + macOS up front 382 | if (sysname == "Windows") 383 | return("windows") 384 | else if (sysname == "Darwin") 385 | return("macos") 386 | 387 | # check for os-release files 388 | for (file in c("/etc/os-release", "/usr/lib/os-release")) 389 | if (file.exists(file)) 390 | return(renv_bootstrap_platform_os_via_os_release(file, sysinfo)) 391 | 392 | # check for redhat-release files 393 | if (file.exists("/etc/redhat-release")) 394 | return(renv_bootstrap_platform_os_via_redhat_release()) 395 | 396 | "unknown" 397 | 398 | } 399 | 400 | renv_bootstrap_platform_os_via_os_release <- function(file, sysinfo) { 401 | 402 | # read /etc/os-release 403 | release <- utils::read.table( 404 | file = file, 405 | sep = "=", 406 | quote = c("\"", "'"), 407 | col.names = c("Key", "Value"), 408 | comment.char = "#", 409 | stringsAsFactors = FALSE 410 | ) 411 | 412 | vars <- as.list(release$Value) 413 | names(vars) <- release$Key 414 | 415 | # get os name 416 | os <- tolower(sysinfo[["sysname"]]) 417 | 418 | # read id 419 | id <- "unknown" 420 | for (field in c("ID", "ID_LIKE")) { 421 | if (field %in% names(vars) && nzchar(vars[[field]])) { 422 | id <- vars[[field]] 423 | break 424 | } 425 | } 426 | 427 | # read version 428 | version <- "unknown" 429 | for (field in c("UBUNTU_CODENAME", "VERSION_CODENAME", "VERSION_ID", "BUILD_ID")) { 430 | if (field %in% names(vars) && nzchar(vars[[field]])) { 431 | version <- vars[[field]] 432 | break 433 | } 434 | } 435 | 436 | # join together 437 | paste(c(os, id, version), collapse = "-") 438 | 439 | } 440 | 441 | renv_bootstrap_platform_os_via_redhat_release <- function() { 442 | 443 | # read /etc/redhat-release 444 | contents <- readLines("/etc/redhat-release", warn = FALSE) 445 | 446 | # infer id 447 | id <- if (grepl("centos", contents, ignore.case = TRUE)) 448 | "centos" 449 | else if (grepl("redhat", contents, ignore.case = TRUE)) 450 | "redhat" 451 | else 452 | "unknown" 453 | 454 | # try to find a version component (very hacky) 455 | version <- "unknown" 456 | 457 | parts <- strsplit(contents, "[[:space:]]")[[1L]] 458 | for (part in parts) { 459 | 460 | nv <- tryCatch(numeric_version(part), error = identity) 461 | if (inherits(nv, "error")) 462 | next 463 | 464 | version <- nv[1, 1] 465 | break 466 | 467 | } 468 | 469 | paste(c("linux", id, version), collapse = "-") 470 | 471 | } 472 | 473 | renv_bootstrap_library_root_name <- function(project) { 474 | 475 | # use project name as-is if requested 476 | asis <- Sys.getenv("RENV_PATHS_LIBRARY_ROOT_ASIS", unset = "FALSE") 477 | if (asis) 478 | return(basename(project)) 479 | 480 | # otherwise, disambiguate based on project's path 481 | id <- substring(renv_bootstrap_hash_text(project), 1L, 8L) 482 | paste(basename(project), id, sep = "-") 483 | 484 | } 485 | 486 | renv_bootstrap_library_root <- function(project) { 487 | 488 | path <- Sys.getenv("RENV_PATHS_LIBRARY", unset = NA) 489 | if (!is.na(path)) 490 | return(path) 491 | 492 | path <- Sys.getenv("RENV_PATHS_LIBRARY_ROOT", unset = NA) 493 | if (!is.na(path)) { 494 | name <- renv_bootstrap_library_root_name(project) 495 | return(file.path(path, name)) 496 | } 497 | 498 | prefix <- renv_bootstrap_profile_prefix() 499 | paste(c(project, prefix, "renv/library"), collapse = "/") 500 | 501 | } 502 | 503 | renv_bootstrap_validate_version <- function(version) { 504 | 505 | loadedversion <- utils::packageDescription("renv", fields = "Version") 506 | if (version == loadedversion) 507 | return(TRUE) 508 | 509 | # assume four-component versions are from GitHub; three-component 510 | # versions are from CRAN 511 | components <- strsplit(loadedversion, "[.-]")[[1]] 512 | remote <- if (length(components) == 4L) 513 | paste("rstudio/renv", loadedversion, sep = "@") 514 | else 515 | paste("renv", loadedversion, sep = "@") 516 | 517 | fmt <- paste( 518 | "renv %1$s was loaded from project library, but this project is configured to use renv %2$s.", 519 | "Use `renv::record(\"%3$s\")` to record renv %1$s in the lockfile.", 520 | "Use `renv::restore(packages = \"renv\")` to install renv %2$s into the project library.", 521 | sep = "\n" 522 | ) 523 | 524 | msg <- sprintf(fmt, loadedversion, version, remote) 525 | warning(msg, call. = FALSE) 526 | 527 | FALSE 528 | 529 | } 530 | 531 | renv_bootstrap_hash_text <- function(text) { 532 | 533 | hashfile <- tempfile("renv-hash-") 534 | on.exit(unlink(hashfile), add = TRUE) 535 | 536 | writeLines(text, con = hashfile) 537 | tools::md5sum(hashfile) 538 | 539 | } 540 | 541 | renv_bootstrap_load <- function(project, libpath, version) { 542 | 543 | # try to load renv from the project library 544 | if (!requireNamespace("renv", lib.loc = libpath, quietly = TRUE)) 545 | return(FALSE) 546 | 547 | # warn if the version of renv loaded does not match 548 | renv_bootstrap_validate_version(version) 549 | 550 | # load the project 551 | renv::load(project) 552 | 553 | TRUE 554 | 555 | } 556 | 557 | renv_bootstrap_profile_load <- function(project) { 558 | 559 | # if RENV_PROFILE is already set, just use that 560 | profile <- Sys.getenv("RENV_PROFILE", unset = NA) 561 | if (!is.na(profile) && nzchar(profile)) 562 | return(profile) 563 | 564 | # check for a profile file (nothing to do if it doesn't exist) 565 | path <- file.path(project, "renv/local/profile") 566 | if (!file.exists(path)) 567 | return(NULL) 568 | 569 | # read the profile, and set it if it exists 570 | contents <- readLines(path, warn = FALSE) 571 | if (length(contents) == 0L) 572 | return(NULL) 573 | 574 | # set RENV_PROFILE 575 | profile <- contents[[1L]] 576 | if (nzchar(profile)) 577 | Sys.setenv(RENV_PROFILE = profile) 578 | 579 | profile 580 | 581 | } 582 | 583 | renv_bootstrap_profile_prefix <- function() { 584 | profile <- renv_bootstrap_profile_get() 585 | if (!is.null(profile)) 586 | return(file.path("renv/profiles", profile)) 587 | } 588 | 589 | renv_bootstrap_profile_get <- function() { 590 | profile <- Sys.getenv("RENV_PROFILE", unset = "") 591 | renv_bootstrap_profile_normalize(profile) 592 | } 593 | 594 | renv_bootstrap_profile_set <- function(profile) { 595 | profile <- renv_bootstrap_profile_normalize(profile) 596 | if (is.null(profile)) 597 | Sys.unsetenv("RENV_PROFILE") 598 | else 599 | Sys.setenv(RENV_PROFILE = profile) 600 | } 601 | 602 | renv_bootstrap_profile_normalize <- function(profile) { 603 | 604 | if (is.null(profile) || profile %in% c("", "default")) 605 | return(NULL) 606 | 607 | profile 608 | 609 | } 610 | 611 | # load the renv profile, if any 612 | renv_bootstrap_profile_load(project) 613 | 614 | # construct path to library root 615 | root <- renv_bootstrap_library_root(project) 616 | 617 | # construct library prefix for platform 618 | prefix <- renv_bootstrap_platform_prefix() 619 | 620 | # construct full libpath 621 | libpath <- file.path(root, prefix) 622 | 623 | # attempt to load 624 | if (renv_bootstrap_load(project, libpath, version)) 625 | return(TRUE) 626 | 627 | # load failed; inform user we're about to bootstrap 628 | prefix <- paste("# Bootstrapping renv", version) 629 | postfix <- paste(rep.int("-", 77L - nchar(prefix)), collapse = "") 630 | header <- paste(prefix, postfix) 631 | message(header) 632 | 633 | # perform bootstrap 634 | bootstrap(version, libpath) 635 | 636 | # exit early if we're just testing bootstrap 637 | if (!is.na(Sys.getenv("RENV_BOOTSTRAP_INSTALL_ONLY", unset = NA))) 638 | return(TRUE) 639 | 640 | # try again to load 641 | if (requireNamespace("renv", lib.loc = libpath, quietly = TRUE)) { 642 | message("* Successfully installed and loaded renv ", version, ".") 643 | return(renv::load()) 644 | } 645 | 646 | # failed to download or load renv; warn the user 647 | msg <- c( 648 | "Failed to find an renv installation: the project will not be loaded.", 649 | "Use `renv::activate()` to re-initialize the project." 650 | ) 651 | 652 | warning(paste(msg, collapse = "\n"), call. = FALSE) 653 | 654 | }) 655 | -------------------------------------------------------------------------------- /renv/settings.dcf: -------------------------------------------------------------------------------- 1 | external.libraries: 2 | ignored.packages: 3 | package.dependency.fields: Imports, Depends, LinkingTo 4 | r.version: 5 | snapshot.type: implicit 6 | use.cache: TRUE 7 | vcs.ignore.library: TRUE 8 | vcs.ignore.local: TRUE 9 | -------------------------------------------------------------------------------- /rsconnect/shinyapps.io/rszymanski/table-contest.dcf: -------------------------------------------------------------------------------- 1 | name: table-contest 2 | title: table-contest 3 | username: 4 | account: rszymanski 5 | server: shinyapps.io 6 | hostUrl: https://api.shinyapps.io/v1 7 | appId: 5059054 8 | bundleId: 5221353 9 | url: https://rszymanski.shinyapps.io/table-contest/ 10 | when: 1636214828.7692 11 | lastSyncTime: 1636214828.76921 12 | asMultiple: FALSE 13 | asStatic: FALSE 14 | ignoredFiles: .Rprofile 15 | -------------------------------------------------------------------------------- /styles/main.scss: -------------------------------------------------------------------------------- 1 | // Colors 2 | $white: white; 3 | $secondary-gray: hsl(201, 23%, 34%); 4 | $tertiary-gray: hsl(203, 15%, 47%); 5 | $background-gray: hsl(210, 0%, 88%); 6 | 7 | $alive-color: hsl(116, 60%, 90%); 8 | $alive-text-color: hsl(116, 30%, 25%); 9 | 10 | $unknown-color: hsl(230, 70%, 90%); 11 | $unknown-text-color: hsl(230, 45%, 30%); 12 | 13 | $dead-color: hsl(350, 70%, 90%); 14 | $dead-text-color: hsl(350, 45%, 30%); 15 | 16 | $mars-icon-color: lightblue; 17 | $venus-icon-color: pink; 18 | $genderless-icon-color: gray; 19 | $question-icon-color: hsl(31, 100%, 70%); 20 | 21 | // Radiuses 22 | $default-radius: 16px; 23 | 24 | // Font sizes and weights 25 | $font-size-s: 12px; 26 | $font-size-m: 16px; 27 | $font-size-l: 18px; 28 | 29 | $weight-regular: 500; 30 | $weight-emphasized: 700; 31 | 32 | // Spacing 33 | $space-xs: 4px; 34 | $space-s: 8px; 35 | $space-m: 12px; 36 | $space-l: 18px; 37 | $space-xl: 24px; 38 | $space-xxl: 32px; 39 | 40 | // Sizing 41 | $character-image-size: 80px; 42 | 43 | body { 44 | background-color: $background-gray; 45 | } 46 | 47 | .selectize-input { 48 | border-radius: $default-radius; 49 | 50 | &.input-active { 51 | border-radius: $default-radius; 52 | } 53 | } 54 | 55 | .seclectize-dropdown { 56 | border-radius: $default-radius; 57 | } 58 | 59 | .selectize-dropdown-content { 60 | border-radius: $default-radius; 61 | } 62 | 63 | .form-control { 64 | border-radius: $default-radius; 65 | } 66 | 67 | .round-box { 68 | background-color: $white; 69 | border-radius: $default-radius; 70 | padding: $space-xxl; 71 | height: calc(70vh + 64px + 35px); 72 | } 73 | 74 | .pagination-control { 75 | border: 0px; 76 | outline: 0px!important; 77 | font-size: $font-size-l; 78 | color: $tertiary-gray; 79 | 80 | &:active { 81 | color: $tertiary-gray; 82 | } 83 | 84 | &:focus { 85 | background-color: transparent; 86 | color: $tertiary-gray; 87 | } 88 | } 89 | 90 | .placeholder-icon { 91 | font-weight: $weight-regular; 92 | color: $tertiary-gray; 93 | font-size: 150px; 94 | } 95 | 96 | .placeholder-text { 97 | font-weight: $weight-emphasized; 98 | font-size: $font-size-l; 99 | padding-top: $space-m; 100 | color: $tertiary-gray; 101 | } 102 | 103 | .pages-text { 104 | color: $tertiary-gray; 105 | font-weight: $weight-emphasized; 106 | margin-right: $space-xl; 107 | } 108 | 109 | %status-badge { 110 | border-radius: $default-radius; 111 | padding: $space-xs $space-m; 112 | } 113 | 114 | .status-badge-alive { 115 | @extend %status-badge; 116 | background-color: $alive-color; 117 | color: $alive-text-color; 118 | } 119 | 120 | .status-badge-unknown { 121 | @extend %status-badge; 122 | background-color: $unknown-color; 123 | color: $unknown-text-color; 124 | } 125 | 126 | .status-badge-dead { 127 | @extend %status-badge; 128 | background-color: $dead-color; 129 | color: $dead-text-color; 130 | } 131 | 132 | .character-image { 133 | height: $character-image-size; 134 | border-radius: 50%; 135 | } 136 | 137 | .character-image-column { 138 | width: calc($character-image-size + $space-s); 139 | max-width: calc($character-image-size + $space-s); 140 | } 141 | 142 | .character-table-header { 143 | text-transform: uppercase; 144 | color: $tertiary-gray; 145 | } 146 | 147 | .filters-container { 148 | display: flex; 149 | justify-content: space-around 150 | } 151 | 152 | .table-placeholder { 153 | display: flex; 154 | flex-direction: column; 155 | align-items: center; 156 | justify-content: center; 157 | height: 100% 158 | } 159 | 160 | .pagination-container { 161 | display: flex; 162 | justify-content: flex-end; 163 | align-items: center 164 | } 165 | 166 | .gender-cell-content { 167 | font-size: $font-size-m; 168 | } 169 | 170 | %gender-cell-icon { 171 | margin-right: $space-s; 172 | } 173 | 174 | .mars-icon { 175 | @extend %gender-cell-icon; 176 | color: $mars-icon-color; 177 | } 178 | 179 | .venus-icon { 180 | @extend %gender-cell-icon; 181 | color: $venus-icon-color; 182 | } 183 | 184 | .genderless-icon { 185 | @extend %gender-cell-icon; 186 | color: $genderless-icon-color; 187 | } 188 | 189 | .question-icon { 190 | @extend %gender-cell-icon; 191 | color: $question-icon-color; 192 | } 193 | 194 | .name-container { 195 | font-weight: $weight-emphasized; 196 | font-size: $font-size-m; 197 | } 198 | 199 | .species-container { 200 | color: $secondary-gray; 201 | font-size: $font-size-m; 202 | } 203 | 204 | .type-container { 205 | color: $tertiary-gray; 206 | font-size: 14px 207 | } -------------------------------------------------------------------------------- /table-contest.Rproj: -------------------------------------------------------------------------------- 1 | Version: 1.0 2 | 3 | RestoreWorkspace: Default 4 | SaveWorkspace: Default 5 | AlwaysSaveHistory: Default 6 | 7 | EnableCodeIndexing: Yes 8 | UseSpacesForTab: Yes 9 | NumSpacesForTab: 2 10 | Encoding: UTF-8 11 | 12 | RnwWeave: Sweave 13 | LaTeX: pdfLaTeX 14 | -------------------------------------------------------------------------------- /www/main.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: #e0e0e0; 3 | } 4 | 5 | .selectize-input { 6 | border-radius: 16px; 7 | } 8 | 9 | .selectize-input.input-active { 10 | border-radius: 16px; 11 | } 12 | 13 | .seclectize-dropdown { 14 | border-radius: 16px; 15 | } 16 | 17 | .selectize-dropdown-content { 18 | border-radius: 16px; 19 | } 20 | 21 | .form-control { 22 | border-radius: 16px; 23 | } 24 | 25 | .round-box { 26 | background-color: white; 27 | border-radius: 16px; 28 | padding: 32px; 29 | height: calc(70vh + 64px + 35px); 30 | } 31 | 32 | .pagination-control { 33 | border: 0px; 34 | outline: 0px !important; 35 | font-size: 18px; 36 | color: #667c8a; 37 | } 38 | 39 | .pagination-control:active { 40 | color: #667c8a; 41 | } 42 | 43 | .pagination-control:focus { 44 | background-color: transparent; 45 | color: #667c8a; 46 | } 47 | 48 | .placeholder-icon { 49 | font-weight: 500; 50 | color: #667c8a; 51 | font-size: 150px; 52 | } 53 | 54 | .placeholder-text { 55 | font-weight: 700; 56 | font-size: 18px; 57 | padding-top: 12px; 58 | color: #667c8a; 59 | } 60 | 61 | .pages-text { 62 | color: #667c8a; 63 | font-weight: 700; 64 | margin-right: 24px; 65 | } 66 | 67 | .status-badge-dead, .status-badge-unknown, .status-badge-alive { 68 | border-radius: 16px; 69 | padding: 4px 12px; 70 | } 71 | 72 | .status-badge-alive { 73 | background-color: #d8f5d6; 74 | color: #2f532d; 75 | } 76 | 77 | .status-badge-unknown { 78 | background-color: #d4daf7; 79 | color: #2a366f; 80 | } 81 | 82 | .status-badge-dead { 83 | background-color: #f7d4da; 84 | color: #6f2a36; 85 | } 86 | 87 | .character-image { 88 | height: 80px; 89 | border-radius: 50%; 90 | } 91 | 92 | .character-image-column { 93 | width: calc($character-image-size + $space-s); 94 | max-width: calc($character-image-size + $space-s); 95 | } 96 | 97 | .character-table-header { 98 | text-transform: uppercase; 99 | color: #667c8a; 100 | } 101 | 102 | .filters-container { 103 | display: flex; 104 | justify-content: space-around; 105 | } 106 | 107 | .table-placeholder { 108 | display: flex; 109 | flex-direction: column; 110 | align-items: center; 111 | justify-content: center; 112 | height: 100%; 113 | } 114 | 115 | .pagination-container { 116 | display: flex; 117 | justify-content: flex-end; 118 | align-items: center; 119 | } 120 | 121 | .gender-cell-content { 122 | font-size: 16px; 123 | } 124 | 125 | .question-icon, .genderless-icon, .venus-icon, .mars-icon { 126 | margin-right: 8px; 127 | } 128 | 129 | .mars-icon { 130 | color: lightblue; 131 | } 132 | 133 | .venus-icon { 134 | color: pink; 135 | } 136 | 137 | .genderless-icon { 138 | color: gray; 139 | } 140 | 141 | .question-icon { 142 | color: #ffb566; 143 | } 144 | 145 | .name-container { 146 | font-weight: 700; 147 | font-size: 16px; 148 | } 149 | 150 | .species-container { 151 | color: #435d6b; 152 | font-size: 16px; 153 | } 154 | 155 | .type-container { 156 | color: #667c8a; 157 | font-size: 14px; 158 | } 159 | --------------------------------------------------------------------------------