Server: Reactive programming

Sources: @wickham2021mastering [Ch. 3]

พื้นฐาน

  • ใน Shiny ตรรกะของ server ถูกเขียนโดยใช้รูปแบบที่เรียกว่า reactive programming
  • ซึ่งมันจะต่างจากเขียน code ที่รันให้ทำอะไรตั้งแต่ต้นจนจบ
  • ไอเดียสำคัญ: เมื่อ inputs ของส่วนที่สนใจเปลี่ยน outputs ที่เกี่ยวข้องก็จะเปลี่ยนตามเองอัตโนมัติ
  • Reactive programming ช่วยให้เขียน code ได้ง่ายขึ้น (เขาว่ามา…ไม่รู้จริงหรือเปล่า)

server function

  • หัวใจหลักของทุกๆ shiny app
library(shiny)

# front end interface (Html)
1ui <- fluidPage()

# back end logic
2server <- function(input, output, session) {}

shinyApp(ui, server)
1
User interface
2
Server
  • ui ง่ายเพราะผู้ใช้ทุกคนจะได้ code html เดียวกัน
  • server ค่อนข้างซับซ้อน เพราะแต่ล่ะคนที่ใช้จะไม่เหมือนกัน
    • เช่น ส้มกำลังใช้งาน slider ก็ไม่ควรจะไปมีผลอะไรกับการใช้งานของสมภพ
  • server() จะถูกเรียกทุกครั้งที่เริ่มใช้งาน
    • ตัวแปร 3 ตัวนี้ (input, output, session) ที่จะถูกสร้างอัตโนมัติโดย Shiny app เมื่อเริ่มต้นใช้งาน

Input & output (lists)

  • input: เป็นตัวแปรแบบ list ที่จะรับ input ทุกตัวที่ส่งมาจาก browser โดย input ที่มาจะมีชื่อตาม input ID
    • เช่น numericInput("count", label = "Number of values", value = 100) จะสร้าง input$count
    • input สามารถอ่านค่าได้จากภายใน reactive functions อย่าง renderText() หรือ reactive()
      • reactive functions ทำให้ค่า outputs เปลี่ยนอัตโนมัติเมื่อ input เปลี่ยน
  • output: เป็นตัวแปรแบบ list ที่ประกอบด้วย outputs ตามชื่อ output ID
    • ความแต่กต่าง: output ใช้สำหรับส่งค่า output แทนที่จะรับ input (ส่วนใหญ่จะเป็นพวก render function)
    • Q: ใน code ด้านล่างนี้มี inputs/outputs/render functions กี่ที่ และมันทำหน้าที่อะไร
Simple input/output example
ui <- fluidPage(
  textInput("name", "What's your name?"),
  textOutput("greeting")
)

server <- function(input, output, session) {
1  output$greeting <- renderText({
    paste0("Hello ", input$name, "!")
  })
}
shinyApp(ui, server)
1
render functions อย่าง renderText() ทำหน้าที่ convert ผลลัพธ์เป็น HTML ให้ตรงตามที่ต้องการบนหน้า web page

Render functions

  • render functions จะอยู่ในส่วนของ server ทำหน้าที่สร้างหรือคอยอัพเดท outputs ตามชนิดที่ต้องการ
    • ค่าผลลัพธ์จะถูกเก็บไว้ที่ output$...
  • Q: render functions พวกนี้ใช้ทำอะไร
    • renderImage({...})
    • renderPlot({...})
    • renderPlotly({...}) (!)
    • renderPrint({...})
    • renderTable({...})
    • renderDataTable({...}) (!)
    • renderText({...})
    • renderUI({...}) (!)
    • renderLeaflet({...}) (!)
  • พวกที่มีเครื่องหมาย(!) คืออันที่เราใช้ใน Guerry app
  • renderImage({...}) สร้าง output แบบ images
  • renderPlot({...}) สร้าง plots
  • renderPlotly({...}) สร้าง interactive plotly กราฟต่างๆ
  • renderPrint({...}) สำหรับ output จากคำสั่ง print
  • renderTable({...}) สำหรับ data frame, matrix, หรือพวกตารางต่างๆ
    • renderDataTable({...}) สร้าง interactive datatable
  • renderText({...}) สำหรับแสดงข้อความ
  • renderUI({...}) สร้างพวก UI
  • renderLeaflet({...}) สร้างแผนที่ leaflet

Reactive programming

reactivity มันทำงานอย่างไร

  • Q: reactivity มันทำงานอย่างไร และ app ด้านล่างนี้มันทำอะไร
ui <- fluidPage(
  textInput("name", "What's your name?"),
  textOutput("greeting")
)

server <- function(input, output, session) {
  output$greeting <- renderText({
    paste0("Hello ", input$name, "!")
  })
}
shinyApp(ui, server)
  • Shiny จะทำการแสดงผล renderText() ทุกครั้งที่เราอัพเดทค่า input$name
  • reactive หมายถึงตัวมันเองจะเปลี่ยนเองอัตโนมัติ เมื่อค่าที่มันเกี่ยวข้องมีการเปลี่ยนแปลง
  • Important: Code เป็นเพียงตัวบอกว่ามันจะสร้างข้อความอย่างไร แต่มันขึ้นกับตัว Shiny เองว่าจะรัน code เมื่อไหร่ตอนไหน
  • Recipe: App เป็นเพียงตัวที่บอก Shiny ว่าจะทำอะไร อย่างไรกับ inputs

Reactive Graph

  • โดยปกติเราจะอ่าน R code จากบนลงล่าง(= ตามลำดับคำสั่ง)… แต่ไม่ใช่กับ Shiny!
  • Reactive graph: เป็นตัวอธิบายว่า inputs และ outputs มันเชื่อมต่อกันเพื่อรันคำสั่งตามลำดับอย่างไร
  • Figure 1 อธิบาย app ใน Section 5.1
    • output$greeting จะมีการคำนวณใหม่ทุกครั้งที่ input$name มีการเปลี่ยน
    • greeting มันจะขึ้นกับname

Figure 1: Reactive graph แสดงให้เห็นว่า inputs และ outputs สัมพันธ์กันอย่างไร (ที่มา: Wickham 2021)
  • การร่างแบบของ reactive graphs จะช่วยให้เข้าในแต่ล่ะส่วนได้ดีขึ้น

Reactive expressions

  • Reactive expressions รับค่า inputs แล้วก็สร้าง outputs
    • สารถช่วยลดการซ้ำซ้อนของ code ได้ด้วยการใส่ nodes ที่ต้องการใน reactive graph
    • Figure 2 ประกอบด้วย reactive expression string ดู code ด้านล่าง

Figure 2: reactive expression จะถูกวาดมีมุมทั้งสองด้านเพราะมันจะเป็นตัวเชื่อมระหว่าง inputs ไปยัง outputs (ที่มา: Wickham 2021)
  • จากด้านล่าง ตัว string ถูกสร้างแบบ reactive() function ใน Section 5.1 อยากให้สังเกตการเรียกค่าไปใช้
ui <- fluidPage(
  textInput("name", "What's your name?"),
  textOutput("greeting"),
)

server <- function(input, output, session) {
  string <- reactive(paste0("Hello ", input$name, "!"))
  output$greeting <- renderText(string())
}
shinyApp(ui, server)
  • หลีกเลี่ยงการทำซ้ำ
    • Q: code ด้านล่างนี้หลีกเลี่ยงการทำซ้ำได้อย่างไร
Show the code
ui <- fluidPage(
  textInput("name", "What's your name?"),
  textOutput("greeting"),
  textOutput("greeting2")
)

server <- function(input, output, session) {
  string <- reactive(paste0("Hello ", input$name, "!"))
  output$greeting <- renderText(string())
  output$greeting2 <- renderText(string())
}
shinyApp(ui, server)

รันหรือทำตามลำดับ

  • ลำดับของ code ใน Shiny มันรันตาม reactive graph
  • ลองสลับลำดับของ code ใน server
    • พยายามเขียนตามลำดับให้เข้าใจง่ายเข้าไว้!
ui <- fluidPage(
  textInput("name", "What's your name?"),
  textOutput("greeting"),
)

server <- function(input, output, session) {
  output$greeting <- renderText(string())
  string <- reactive(paste0("Hello ", input$name, "!"))
}
shinyApp(ui, server)

Exercises

  1. code ด้านล่าง ในส่วนของ server1, server2 และ server3 นี้ผิดตรงไหน
# UI
ui <- fluidPage(
  textInput("name", "What's your name?"),
  textOutput("greeting")
)

# SERVERS
server1 <- function(input, output, server) {
  input$greeting <- renderText(paste0("Hello ", name))
}

# HOMEWORK!
server2 <- function(input, output, server) {
  greeting <- paste0("Hello ", input$name)
  output$greeting <- renderText(greeting)
}

server3 <- function(input, output, server) {
  output$greting <- paste0("Hello", input$name)
}
  1. เขียน reactive graph สำหรับ server functions (ว่าอะไรคือ inputs, reactives และ ouputs): (Homework: server2 กับ server3!)

เริ่มด้วยการดูว่ามี inputs (1), reactives (2) และ ouputs (3) กี่ที่ตรงไหนบ้าง และทำหน้าที่อะไร จากนั้นเริ่มเขียน inputs ใน column แรกทางซ้าย เราอาจจะใช้ อย่างเช่น name> สำหรับ inputs >name> สำหรับ reactives และ >name สำหรับ outputs และใช้ลูกศรเชื่อม

server1 <- function(input, output, session) {
  c <- reactive(input$a + input$b)
  e <- reactive(c() + input$d)
  output$f <- renderText(e())
}

server2 <- function(input, output, session) {
  x <- reactive(input$x1 + input$x2 + input$x3)
  y <- reactive(input$y1 + input$y2)
  output$z <- renderText(x() / y())
}

server3 <- function(input, output, session) {
  d <- reactive(c() ^ input$d)
  a <- reactive(input$a * 10)
  c <- reactive(b() / input$c) 
  b <- reactive(a() + input$b)
}
  1. code ผิดตรงไหน ใน server1, server2 และ server3
  • server1: ลืม input$
  • server2: input$name อยู่นอก renderText() function
  • server3: พิมพ์ผิด output$greting




  1. เขียน reactive graph สำหรับ server functions ทั้ง3 (Solution source):

ในการสร้าง reactive graph เราต้องพิจารณา inputs, reactive expressions, และ outputs ของ app

สำหรับ server1 เรามี:

  • inputs: input$a, input$b, และ input$d
  • reactives: c() และ e()
  • outputs: output$f

Inputs input$a และ input$b ถูกใช้สร้าง c() ซึ่งรวมกับ input$d เพื่อสร้าง e() และ สุดท้าย output จะขึ้นกับ e()

reactive graph - server 1


สำหรับ server2 เรามี:

  • inputs: input$y1, input$y2, input$x1, input$x2, input$x3
  • reactives: y() and x()
  • outputs: output$z

Inputs input$y1 กับ input$y2 ถูกใช้สร้าง reactive y()และ inputs input$x1, input$x2 กับ input$x3 ถูกใช้สร้าง reactive x() ดังนั้น output ขึ้นกับค่า x() และ y()

reactive graph - server 2


สำหรับ server3 เรามี:

  • inputs: input$a, input$b, input$c, input$d
  • reactives: a(), b(), c(), d()

ซึ่งจากด้านล่างเราจะเห็นว่า a() ขึ้นกับค่า input$a และ b() ก็ขึ้นกับ a() และ input$b ส่วน c() ขึ้นกับ b() และ input$c และ output สุดท้ายขึ้นกับทั้ง c() และ input$d

reactive graph - server 3


Reactive expressions (more!)

  • แนะนำให้อ่าน Chapter 3.4!
  • Reactive expressions (เช่น reactive()) มันสำคัญเพราะ…
    • มันช่วยให้ข้อมูล Shiny ว่าจะต้องรันในส่วนไหนบ้างโดยที่มันไม่ต้องรันทั้งหมดเมื่อ inputs เปลี่ยน
    • ช่วยให้ apps มีประสิทธิภาพมากขึ้น และ code อ่านเข้าใจได้ง่าย
  • มันเหมือน inputs ที่เราสามารถใช้ results ของ reacive expression ใน output ได้
  • เหมือนกับ outputs ที่มันขึ้นกับ inputs และมันรู้เองอัตโนมัติว่ามันต้องอัพเดทค่าเมื่อไหร่
  • ตัว Inputs กับ reactive expressions ก็คือ reactive producers (ดู Figure 3)
  • Reactive expressions กับ outputs คือ reactive consumers (ดู Figure 3)

Figure 3: Inputs กับ expressions คือ reactive producers (ตัวสร้าง); expressions กับ outputs คือ reactive consumers (ตัวใช้) (ที่มา: Wickham 2021)

Reactive functions: ภาพรวม

  • Shiny มี reactive functions หลากหลาย เช่น reactive(), observe(), bindevent() และอื่นๆ`
  • อยากเข้าใจเรื่อง reactive นี้มากขึ้นลองดูที่ Chapter 3.5.1.

reactive()

  • reactive(): สำหรับสร้าง reactive expression
    • ในที่นี้ “reactive” มีความหมายในเชิงว่าถ้าค่าของตัวแปรอะไรที่ขึ้นกับมันนี้เปลี่ยน ตัวมันเองจะเปลี่ยนค่าตามอัตโนมัติ
    • ด้านล่างนี้ตัว reactive string จะเปลี่ยนค่าทุกครั้งที่ input$name เปลี่ยนค่า
    • Q: อะไรที่ควรจะเป็นตัว reactive producer และ ตัว reactive consumer
ui <- fluidPage(
  textInput("name", "What's your name?"),
  textOutput("greeting"),
)

server <- function(input, output, session) {
  string <- reactive(paste0("Hello ", input$name, "!"))
  output$greeting <- renderText(string())
}
shinyApp(ui, server)
  })

observe() vs. reactive()

  • reactive(): สร้าง reactive expression ที่ค่าจะเปลี่ยนตามinputsจากผู้ใช้
  • observe(): สร้างตัวสังเกตุที่จะรันเฉพาะเมื่อตัว reactive ที่ขึ้นกับมันเปลี่ยน
    • เช่น code ใน observe() จะรันอีกครั้งถ้าตัว reactive inputs หรือ reactive expressions ที่มันอ้างถึงมีการอัพเดทค่า
    • ปกติเราจะไม่ให้ค่าผลลัพธ์ observe() กับตัวแปร ดังนั้นเราจะไม่สามารถอ้างถึงมันจาก reactive consumers อันอื่นได้

ด้านล่างนี้เราใช้ reactive expression กับ reactive() เพื่อสร้าง squared จากนั้นก็เอามันไปใช้อีกใน observe() function ซึ่งถูกห่อใน render function อย่างrenderText() อีกทีเพื่อสร้าง output$text

library(shiny)

ui <- fluidPage(
  numericInput("num", "Enter a number", value = 1),
  textOutput("text")
)

server <- function(input, output) {
  # reactive expression
  squared <- reactive({ 
    input$num^2 
  })
  
  # observer
  observe({ 
    output$text <- renderText({
      paste0("The square of ", input$num, " is ", squared())
    })
  })
}

shinyApp(ui = ui, server = server)

จัดการกับเหตุการณ์: bindEvent()

  • bindEvent(): เป็นคำสั่งที่ช่วยจัดการกับเหตุการณ์อย่างง่าย
  • observeEvent() (ตัวสังเกตเหตุการณ์): ใช้เมื่อเราต้องการจะทำอะไรบ้างอย่างเมื่อมีเหตุการณ์เกิดขึ้น เหตุการณ์ในที่นี้ เช่นมีการกดปุ่ม พิมพ์ตัวอักษร ลากเม้าส์ และอื่นๆ (ดูที่ input$button ด้านล่างนี้) แต่เราไม่ได้จะใช้ผลจากactionต่อเหตุการณ์นี้ใน UI

ในตัวอย่าง เมื่อปุ่ม “Generate Random Number” ถูกกด จะมีการสุ่มเลขขึ้นมาตัวหนึ่ง แต่จะไม่แสดงผลทันที ตัว output$randomNumber จะถูกเชื่อมไปยังปุ่ม input$dispButton ด้วย bindEvent() และผลลัพธ์เลขสุ่มจะแสดงก็ต่อเมื่อปุ่ม “Display Random Number” ถูกกด ด้วยวิธีนี้เราสามารถที่จะควบคุม UI ได้ว่าจะอัพเดทผลลัพธ์เมื่อใด

library(shiny)

# Define UI
ui <- fluidPage(
    actionButton("genButton", "Generate Random Number"),
    actionButton("dispButton", "Display Random Number"),
    textOutput("randomNumber")
)

# Define server logic
server <- function(input, output) {
    randNum <- reactiveValues(num = NULL)

    observeEvent(input$genButton, {
        randNum$num <- runif(1) # Generate a random number when genButton is clicked
    })
    
    output$randomNumber <- renderText({ 
        randNum$num # Generate the reactive expression
    }) %>% 
    bindEvent(input$dispButton) # Binding the output$randomNumber reactive expression to dispButton
}

# Run the application 
shinyApp(ui = ui, server = server, options = list(display.mode='showcase'))

eventReactive() (ข้าม!)

  • อีกวิธีคือใช้ bind_event()
  • eventReactive(): เหมือนกับ reactive() แต่จะcode ภายในจะถูกรันเมื่อมีเหตุการณ์ที่สนใจเกิดขึ้น สามารถที่จะใช้ร่วมกับ observeEvent()ได้

ผลลัพธ์จะเหมือนกับ app ก่อนหน้านี้ ตัว eventReactive() function ถูกใช้สร้างตัวแปรแบบ reactive (ตัวเลขสุ่ม) ที่มันจะอัพเดทเฉพาะเมื่อมีการกดปุ่ม “Generate Random Number” เท่านั้น

library(shiny)

# Define UI
ui <- fluidPage(
    actionButton("genButton", "Generate Random Number"),
    actionButton("dispButton", "Display Random Number"),
    textOutput("randomNumber")
)

# Define server logic
server <- function(input, output) {
    randNum <- eventReactive(input$genButton, {
        runif(1) # Generate a random number when genButton is clicked
    })
    
    observeEvent(input$dispButton, {
        output$randomNumber <- renderText({ randNum() }) # Display the random number when dispButton is clicked
    })
}

# Run the application 
shinyApp(ui = ui, server = server, options = list(display.mode='showcase'))

isolate() (ข้าม!)

  • isolate(): ใช้เพื่อเข้าถึงค่าของ reactive expression หรือ input โดยไม่ต้องผ่านค่าอะไรเลย
    • มีประโยชน์ในการเข้าถึงค่า ณ ปัจจุบันของ inputs หรือ reactive expression

ใน app เลขสุ่มจะไม่แสดงผลทันทีหลังจากกดปุ่ม “Generate Random Number” เลขสุ่มที่เกิดขึ้นจะยังไม่มีผลเพราะมันถูก แยกออกมาด้วย isolate() function ซึ่งมันจะแสดงค่าก็ต่อเมื่อ ปุ่ม “Display Random Number” ถูกกด การเปลี่ยนค่าของ randNum$num หลังจากปุ่ม “Display Random Number” ถูกกด จะยังไม่มีผลจนกว่า ปุ่ม “Display Random Number” จะถูกกดอีกครั้ง ฉะนั้น isolate() function ช่วยให้เราใช้หรือเข้าถึงค่าแบบ reactive values โดยที่ไม่ต้องมี trigger

library(shiny)

# Define UI
ui <- fluidPage(
    actionButton("genButton", "Generate Random Number"),
    actionButton("dispButton", "Display Random Number"),
    textOutput("randomNumber")
)

# Define server logic
server <- function(input, output) {
    randNum <- reactiveValues(num = NULL) # Create object to store reactiv values

    observeEvent(input$genButton, {
        randNum$num <- runif(1) # Generate a random number when genButton is clicked
    })
    
    observeEvent(input$dispButton, {
        output$randomNumber <- renderText({ 
          isolate(randNum$num) # Display the random number when dispButton is clicked, but do not reactivity link it
        }) 
    })
}

# Run the application 
shinyApp(ui = ui, server = server, options = list(display.mode='showcase'))

reactiveTimer() (ข้าม!)

  • reactiveTimer(): ใช้สำหรับสร้าง reactive expression ที่จะหยุดสักช่วงเวลาหนึ่ง (เป็น milliseconds) มีประโยชน์กรณีทีเราต้องการให้บางส่วนใน app มีการอัพเดทเป็นช่วงเวลาที่ต้องการ

ในตัวอย่าง autoInvalidate() คือ reactive expression ที่จะทำให้หยุดทุก 1000 milliseconds โดยการใช้autoInvalidate() ข้างใน renderText() เป็นการบอกว่าทุกครั้งที่ autoInvalidate() หยุดทำงาน ก็จะมีการคำนวณและแสดงเวลาใหม่

library(shiny)

# Define UI
ui <- fluidPage(
  textOutput("currentTime")
)

# Define server logic
server <- function(input, output) {
  
  # Define a reactive timer with a 1000ms (1s) interval
  autoInvalidate <- reactiveTimer(1000)
  
  output$currentTime <- renderText({
    autoInvalidate()  # This line causes the reactive expression to be invalidated (and thus re-evaluated) every second
    format(Sys.time(), "%a %b %d %Y %X")  # Display the current time
  })
}

# Run the application 
shinyApp(ui = ui, server = server)

Guerry app (reactivity): Tabulate data tab

  • reactive functions ที่ใช้ใน Guerry app: reactive(), observe(), isolate(), bindEvent().
  • code พื้นฐานสำหรับ Tabulate tab ที่ใช้ใน app
    • ตัว reactive graph เป็นอย่างไรสำหรับ app นี้(มี inputs, reatives, outputs กี่ที่)
    • reactive functions อะไรบ้าง
R code underlying tabulate tab
library(shiny)
library(htmltools)
library(bs4Dash)
library(fresh)
library(waiter)
library(shinyWidgets)
library(Guerry)
library(sf)
library(tidyr)
library(dplyr)
library(RColorBrewer)
library(viridis)
library(leaflet)
library(plotly)
library(jsonlite)
library(ggplot2)
library(GGally)
library(datawizard)
library(parameters)
library(performance)
library(ggdark)
library(modelsummary)

# 1 Data preparation ----

## Load & clean data ----
variable_names <- list(
  Crime_pers = "Crime against persons",  
  Crime_prop =  "Crime against property",  
  Literacy = "Literacy",  
  Donations = "Donations to the poor",  
  Infants = "Illegitimate births",  
  Suicides = "Suicides",  
  Wealth = "Tax / capita",  
  Commerce = "Commerce & Industry",  
  Clergy = "Clergy",  
  Crime_parents = "Crime against parents",  
  Infanticide = "Infanticides",  
  Donation_clergy = "Donations to the clergy",  
  Lottery = "Wager on Royal Lottery",  
  Desertion = "Military desertion",  
  Instruction = "Instruction",  
  Prostitutes = "Prostitutes",  
  Distance = "Distance to paris",  
  Area = "Area",  
  Pop1831 = "Population"
)

data_guerry <- Guerry::gfrance85 %>%
  st_as_sf() %>%
  as_tibble() %>%
  st_as_sf(crs = 27572) %>%
  mutate(Region = case_match(
    Region,
    "C" ~ "Central",
    "E" ~ "East",
    "N" ~ "North",
    "S" ~ "South",
    "W" ~ "West"
  )) %>%
  select(-c("COUNT", "dept", "AVE_ID_GEO", "CODE_DEPT")) %>%
  select(Region:Department, all_of(names(variable_names)))



## Prep data (Tab: Tabulate data) ----
data_guerry_tabulate <- data_guerry %>% 
  st_drop_geometry() %>% 
  mutate(across(.cols = all_of(names(variable_names)), round, 2))



# 3 UI ----

ui <- dashboardPage(
  title = "The Guerry Dashboard",

  ## 3.1 Header ----
  header = dashboardHeader(
    title = tagList(
      span("The Guerry Dashboard", class = "brand-text")
    )
  ),
  ## 3.2 Sidebar ----
  sidebar = dashboardSidebar(
    id = "sidebar",
    sidebarMenu(
      id = "sidebarMenu",
      menuItem(tabName = "tab_tabulate", text = "Tabulate data", icon = icon("table")),
      flat = TRUE
    ),
    minified = TRUE,
    collapsed = TRUE,
    fixed = FALSE,
    skin = "light"
  ),
  ## 3.3 Body ----
  body = dashboardBody(
    tabItems(
      ### 3.3.2 Tab: Tabulate data ----
      tabItem(
        tabName = "tab_tabulate",
        fluidRow(
          #### Inputs(s) ----
          pickerInput(
            "tab_tabulate_select",
            label = "Filter variables",
            choices = setNames(names(variable_names), variable_names),
            options = pickerOptions(
              actionsBox = TRUE,
              windowPadding = c(30, 0, 0, 0),
              liveSearch = TRUE,
              selectedTextFormat = "count",
              countSelectedText = "{0} variables selected",
              noneSelectedText = "No filters applied"
            ),
            inline = TRUE,
            multiple = TRUE
          )
        ),
        hr(),
        #### Output(s) (Data table) ----
        DT::dataTableOutput("tab_tabulate_table")
      )
    ) # end tabItems
  )
)



# 4 Server ----

server <- function(input, output, session) {
  
  ## 4.1 Tabulate data ----
  ### Variable selection ----
  tab <- reactive({
    var <- input$tab_tabulate_select
    data_table <- data_guerry_tabulate
    
    if (!is.null(var)) {
      data_table <- data_table[, c("Region", "Department",var)]
    }
    
    data_table
  })
  
  
  ### Create table----
  dt <- reactive({
    tab <- tab()
    ridx <- ifelse("Department" %in% names(tab), 3, 1)
    DT::datatable(
      tab,
      class = "hover",
      extensions = c("Buttons"),
      selection = "none",
      filter = list(position = "top", clear = FALSE),
      style = "bootstrap4",
      rownames = FALSE,
      options = list(
        dom = "Brtip",
        deferRender = TRUE,
        scroller = TRUE,
        buttons = list(
          list(extend = "copy", text = "Copy to clipboard"),
          list(extend = "pdf", text = "Save as PDF"),
          list(extend = "csv", text = "Save as CSV"),
          list(extend = "excel", text = "Save as JSON", action = DT::JS("
          function (e, dt, button, config) {
            var data = dt.buttons.exportData();
  
            $.fn.dataTable.fileSave(
              new Blob([JSON.stringify(data)]),
              'Shiny dashboard.json'
            );
          }
        "))
        )
      )
    )
  })
  
  ### Render table----
  output$tab_tabulate_table <- DT::renderDataTable(dt(), server = FALSE)
  
  

}

shinyApp(ui, server)

การโหลดสิ่งต่างใน Shiny

code มันถูกรันตอนไหน?

  • code มันถูกรันตอนไหน (ลองดูที่นี่)

  • Code ที่อยู่นอก ui กับ server จะถูกรันทันทีที่ app ถูกเริ่มใช้งาน

  • Code ในส่วน server function จะถูกรันเพียงครั้งเดียวตอนเริ่มต้นเมื่อผู้ใช้เปิด app

  • Code ภายใน render functions จะถูกรันทุกครั้งที่ ผู้ใช้เปลี่ยนค่า inputs (input$...) และ ouput$... ก็เปลี่ยนตาม

  • Q: เราควรจะใส่functionสำหรับโหลดข้อมูลตรงไหน
  • Q: ปัญหาอาจจะเกิดเมื่อเราใส่ผิดที่ ฉะนั้นเราควรจะใส่พวกการจัดการข้อมูลตรงไหน
  • ถ้าเป็นไปได้ไม่ควรใส่ส่วนที่มีการคำนวณเยอะๆ ใน render functions
    • อาจจะต้องมีการเตรียมข้อมูลให้พร้อมก่อนเรียกใช้งานใน reactive functions

ควรจะโหลดอะไรตรงไหน

  • Code นอก server <- function(input, output) {} จะถูกรันครั้งเดียวตอน app เริ่มทำงาน
  • Code ภายใน server <- function(input, output) {} จะรันครั้งเดียวตอนที่ผู้ใช้เปิดapp
  • Code ภายใน render* จะมีการรันใหม่ถ้ามีการอัพเดท inputs ( ดูที่ reactivity)
  • หมายความว่า…
    • โหลด scripts, libraries, กับ data นอก server function (ตรงตำแหน่งเริ่มต้น)
      • เก็บข้อมูลไว้ที่ www/ ของ app
      • เข้าถึงข้อมูลด้วย read.table("www/swiss.csv", sep=",")
      • เข้าถึง ข้อมูล online ด้วยการใส่ url ในคำสั่ง read* function (เช่น read.table())
    • ตัววัตถุจำเพาะ (เช่น วัตถุที่เก็บข้อมูลการใช้งาน) ถูกนิยามภายใน Server function แต่ภายนอก render* ทั้งหลาย
      • เช่น ผู้ใช้ลงทะเบียนข้อมูลตัวเองเป็น input data
    • Code/objects ที่ถูกผลกระทบจาก widgets ควรจะใส่ภายใน render* function
      • Shiny จะรัน code ใน render* ทุกครั้งที่ input widgets มีการเปลี่ยนค่า
  • Avoid พยายามอย่าใส่ code ที่ไม่จำเป็นหรือคำนวณนาน ใน render function มันเป็นเรื่องของประสิทธิภาพ!

ที่เก็บ Data

  • รูปแบบการจัดเก็บข้อมูลมีความสำคัญ
    • Memory allocation: R จะเก็บค่าต่าง ๆ ไว้ที่ working memory
    • Performance: “R ทำหลายอย่างแล้ว” - Colin Fay
    • Readability: ใส่ทุกอย่างไปในไฟล์เดียวมันจะอ่านยาก
  • สำหรับการทำงานกับฐานข้อมูล : databases (เช่น SQLite, PostgreSQL, MongoDB)
  • R สามารถทำงานกับ database (R Packages: DBI, dbplyr, sf) (ดูภาพรวมที่)

สรุป

ในการสร้าง reactive shiny apps…

  • ใช้ *Output เพื่อวาง reactive objects บน UI (webpage)
  • ใช้ render* เพื่อให้ R สร้าง output objects (บน server)
    • Render functions ใส่ใน server <- function(input, output) {...})
    • R expressions จะถูกล้อมด้วยปีกกา {} ใน render* functions
    • ผลลัพธ์จาก render* จะถูกเก็บไว้ที่ output list
    • Reactivity ด้วยการใส่ input ใน render* expression
  • ถ้าอยากจะสร้าง ปรับปรุง เปลี่ยนแปลง app ที่มีอยู่ ลองดูตัวอย่างนี้เป็นแนวทางได้ ตัวอย่าง

Appendix: แสดง reactivity ด้วย reactlog

  • reactlog สามารถใช้เพื่อแสดงผลและสำรวจ reactivity ของ Shiny app
  • ลองดูตัวอย่างนี้
# Restart R to delete log
.rs.restartR()

library(shiny)
library(reactlog)

# tell shiny to log all reactivity
reactlog_enable()
# reactlog_disable()

# run a shiny app
runApp("shinyapps/guerry/states_paul/app_tab_tabulate.R")

# once app has closed, display reactlog from shiny
shiny::reactlogShow()

Appendix: Imperative กับ Declarative programming และ laziness

  • Imperative vs. declarative programming (Chapter 3.3.1)
    • Imperative code: “ทำแซนวิชให้หน่อย”
    • Declarative code: “แน่ใจว่ามีแซนวิชทุกครั้งที่เปิดตู้เย็น”
    • Shiny จะเป็นแบบ Declarative
  • Laziness เป็นจุดแข็งของ declarative programming (Chapter 3.3.2)
    • app มันจะทำงานให้น้อยที่สุดที่จะอัพเดทตัวควบคุมผลลัพธ์