iBetter Books
수정

Ch 04. 차트 클릭과 브러시 이벤트

plotly 없이도 ggplot2 차트에 인터랙션을 넣을 수 있습니다. plotOutput()은 클릭, 호버, 브러시(드래그 선택) 이벤트를 기본으로 지원합니다. 이 기능을 활용하면 차트를 클릭해 상세 정보를 보거나, 드래그로 범위를 선택해 데이터를 필터링하는 패턴을 순수 R만으로 구현할 수 있습니다.

plotOutput의 이벤트 인수

plotOutput(
  outputId = "my_plot",
  click    = "plot_click",    # 클릭 이벤트 ID
  dblclick = "plot_dblclick", # 더블클릭 이벤트 ID
  hover    = "plot_hover",    # 호버(마우스 올리기) 이벤트 ID
  brush    = "plot_brush"     # 브러시(드래그 선택) 이벤트 ID
)

이벤트 ID로 지정한 문자열이 input$의 키가 됩니다. click = "plot_click"이면 input$plot_click으로 클릭 좌표를 읽습니다.

클릭 이벤트와 nearPoints

input$plot_click은 클릭한 위치의 x, y 좌표를 담은 리스트입니다. nearPoints()는 클릭 위치에서 가장 가까운 데이터 포인트를 찾아줍니다.

library(shiny)
library(ggplot2)

ui <- fluidPage(
  titlePanel("차트 클릭으로 상세 정보 보기"),
  fluidRow(
    column(8,
      plotOutput("scatter",
                 click = "point_click",
                 height = "400px")
    ),
    column(4,
      h4("클릭한 데이터"),
      tableOutput("click_detail"),
      p("차트의 점을 클릭하면 해당 차량 정보가 표시됩니다.")
    )
  )
)

server <- function(input, output, session) {
  output$scatter <- renderPlot({
    ggplot(mtcars, aes(x = hp, y = mpg, color = factor(cyl))) +
      geom_point(size = 4, alpha = 0.8) +
      scale_color_manual(
        values = c("4" = "#2ecc71", "6" = "#3498db", "8" = "#e74c3c"),
        name   = "실린더"
      ) +
      labs(title = "마력 vs 연비", x = "마력(hp)", y = "연비(mpg)") +
      theme_minimal(base_size = 14)
  })

  output$click_detail <- renderTable({
    req(input$point_click)

    # 클릭 위치에서 가장 가까운 포인트 찾기
    clicked <- nearPoints(
      df       = mtcars,
      coordinfo = input$point_click,
      xvar     = "hp",
      yvar     = "mpg",
      threshold = 10,     # 픽셀 기준 탐색 반경
      maxpoints = 1       # 최대 반환 포인트 수
    )

    if (nrow(clicked) == 0) return(NULL)

    data.frame(
      항목 = c("차종", "연비(mpg)", "마력(hp)", "실린더", "무게(wt)"),
      값   = c(rownames(clicked),
                clicked$mpg, clicked$hp, clicked$cyl, clicked$wt)
    )
  }, rownames = FALSE)
}

shinyApp(ui, server)

nearPoints()threshold는 픽셀 단위 탐색 반경입니다. 너무 작으면 정확히 클릭해야 하고, 너무 크면 여러 점이 잡힙니다. 10~15 픽셀이 무난합니다.

브러시 이벤트와 brushedPoints

브러시는 마우스를 드래그해서 사각형 영역을 선택하는 기능입니다. brushedPoints()는 브러시 영역 안에 있는 모든 데이터 포인트를 반환합니다.

library(shiny)
library(ggplot2)
library(dplyr)

ui <- fluidPage(
  titlePanel("브러시로 범위 선택"),
  fluidRow(
    column(8,
      p("차트에서 영역을 드래그해 데이터를 선택하세요."),
      plotOutput("main_scatter",
                 brush = brushOpts(
                   id        = "area_brush",
                   fill      = "rgba(52, 152, 219, 0.2)",
                   stroke    = "#3498db",
                   direction = "xy",   # "x", "y", "xy" 중 선택
                   resetOnNew = TRUE
                 ),
                 height = "400px")
    ),
    column(4,
      h4("선택된 데이터"),
      textOutput("selected_count"),
      tableOutput("selected_table")
    )
  )
)

server <- function(input, output, session) {
  output$main_scatter <- renderPlot({
    ggplot(mtcars, aes(x = hp, y = mpg)) +
      geom_point(size = 3, color = "#2c3e50", alpha = 0.7) +
      labs(x = "마력(hp)", y = "연비(mpg)") +
      theme_minimal(base_size = 13)
  })

  # 브러시 영역 안의 데이터
  selected <- reactive({
    brushedPoints(mtcars, input$area_brush,
                  xvar = "hp", yvar = "mpg")
  })

  output$selected_count <- renderText({
    n <- nrow(selected())
    if (n == 0) "선택된 데이터 없음"
    else paste0(n, "개 선택됨")
  })

  output$selected_table <- renderTable({
    req(nrow(selected()) > 0)
    selected()[, c("mpg", "cyl", "hp", "wt")]
  }, rownames = TRUE, digits = 1)
}

shinyApp(ui, server)

brushOpts()로 브러시의 색상과 동작 방향을 지정합니다. direction = "x"이면 가로 범위만, "y"이면 세로 범위만, "xy"이면 사각형 영역을 선택합니다.

선택 강조: 브러시로 선택된 점 색상 변경

가장 실용적인 패턴은 브러시로 선택된 점을 색상으로 구분하고, 선택 결과를 테이블에 보여주는 것입니다.

library(shiny)
library(ggplot2)

ui <- fluidPage(
  plotOutput("linked_scatter",
             brush = "region_brush",
             height = "400px"),
  tableOutput("region_table")
)

server <- function(input, output, session) {
  output$linked_scatter <- renderPlot({
    # 브러시 영역 안의 점을 강조
    in_brush <- brushedPoints(mtcars, input$region_brush,
                               xvar = "hp", yvar = "mpg",
                               allRows = TRUE)  # 전체 행 반환

    ggplot(in_brush, aes(x = hp, y = mpg,
                          color = selected_,   # 선택 여부 열
                          size  = selected_)) +
      geom_point(alpha = 0.8) +
      scale_color_manual(
        values = c("TRUE" = "#e74c3c", "FALSE" = "#95a5a6"),
        guide  = "none"
      ) +
      scale_size_manual(
        values = c("TRUE" = 5, "FALSE" = 2),
        guide  = "none"
      ) +
      labs(x = "마력(hp)", y = "연비(mpg)") +
      theme_minimal()
  })

  output$region_table <- renderTable({
    brushedPoints(mtcars, input$region_brush,
                  xvar = "hp", yvar = "mpg")
  }, rownames = TRUE, digits = 1)
}

shinyApp(ui, server)

brushedPoints(allRows = TRUE)는 선택 여부를 나타내는 selected_ 열이 추가된 전체 데이터프레임을 반환합니다. 이 열을 aes(color = selected_)로 활용해 선택된 점과 그렇지 않은 점을 다르게 표시합니다.

호버 이벤트와 nearPoints

마우스를 올렸을 때 말풍선처럼 정보를 보여주는 패턴입니다.

library(shiny)
library(ggplot2)

ui <- fluidPage(
  plotOutput("hover_scatter",
             hover = hoverOpts(
               id       = "plot_hover",
               delay    = 100,     # 호버 반응 지연(ms)
               delayType = "debounce"  # 또는 "throttle"
             ),
             height = "400px"),
  verbatimTextOutput("hover_info")
)

server <- function(input, output, session) {
  output$hover_scatter <- renderPlot({
    ggplot(mtcars, aes(x = hp, y = mpg)) +
      geom_point(size = 3, color = "#2980b9", alpha = 0.7) +
      theme_minimal()
  })

  output$hover_info <- renderPrint({
    point <- nearPoints(mtcars, input$plot_hover,
                        xvar = "hp", yvar = "mpg",
                        threshold = 8, maxpoints = 1)

    if (nrow(point) == 0) {
      cat("그래프 위에 마우스를 올려보세요.\n")
    } else {
      cat("차종:", rownames(point), "\n")
      cat("마력:", point$hp, "hp\n")
      cat("연비:", point$mpg, "mpg\n")
    }
  })
}

shinyApp(ui, server)

세 가지 이벤트 비교

이벤트 함수 반환 주용도
click nearPoints() 클릭 근처 행 선택 후 상세 정보 조회
brush brushedPoints() 선택 영역 내 행 범위 선택으로 필터링
hover nearPoints() 호버 근처 행 마우스 오버 툴팁

클릭은 명시적 선택, 브러시는 범위 선택, 호버는 탐색에 적합합니다. 세 가지를 조합하면 드래그로 범위를 좁히고, 클릭으로 하나를 고르고, 호버로 미리 보는 대화형 탐색 환경을 완성할 수 있습니다.