iBetter Books
수정

Ch 01. plotly로 인터랙티브 차트

ggplot2로 멋진 그래프를 만들었는데, 사용자가 "이 점이 어떤 데이터인지 알고 싶다"고 물어봅니다. 정적 이미지로는 답하기 어렵습니다. plotly는 차트를 인터랙티브하게 만들어 마우스를 올리면 값이 보이고, 드래그하면 줌이 되며, 클릭하면 특정 데이터를 강조할 수 있습니다.

plotly 설치

install.packages("plotly")

ggplotly: ggplot2를 한 줄로 인터랙티브로

가장 빠른 방법은 완성된 ggplot2 객체를 ggplotly()로 감싸는 것입니다.

library(shiny)
library(ggplot2)
library(plotly)

ui <- fluidPage(
  titlePanel("ggplotly 예제"),
  plotlyOutput("scatter")  # plotOutput이 아닌 plotlyOutput
)

server <- function(input, output, session) {
  output$scatter <- renderPlotly({  # renderPlot이 아닌 renderPlotly
    p <- ggplot(mtcars, aes(x = hp, y = mpg,
                             color = factor(cyl),
                             text = rownames(mtcars))) +
      geom_point(size = 3) +
      labs(title  = "마력 vs 연비",
           x      = "마력(hp)",
           y      = "연비(mpg)",
           color  = "실린더") +
      theme_minimal()

    ggplotly(p, tooltip = c("text", "x", "y"))
  })
}

shinyApp(ui, server)

plotOutput/renderPlot 대신 plotlyOutput/renderPlotly를 씁니다. ggplotly()tooltip 인수로 호버 시 표시할 정보를 지정합니다. aes(text = ...)로 커스텀 호버 텍스트를 추가할 수 있습니다.

plot_ly로 직접 그리기

plot_ly()를 사용하면 plotly 네이티브 방식으로 더 세밀하게 제어할 수 있습니다.

library(shiny)
library(plotly)
library(dplyr)

ui <- fluidPage(
  selectInput("chart_type", "차트 종류",
              choices = c("막대" = "bar",
                          "산점도" = "scatter",
                          "히스토그램" = "histogram")),
  plotlyOutput("chart", height = "450px")
)

server <- function(input, output, session) {
  output$chart <- renderPlotly({
    if (input$chart_type == "bar") {
      mtcars %>%
        count(cyl) %>%
        plot_ly(x = ~factor(cyl), y = ~n,
                type  = "bar",
                color = ~factor(cyl),
                text  = ~paste("실린더:", cyl, "<br>대수:", n),
                hoverinfo = "text") %>%
        layout(title     = "실린더별 자동차 수",
               xaxis     = list(title = "실린더 수"),
               yaxis     = list(title = "대수"),
               showlegend = FALSE)

    } else if (input$chart_type == "scatter") {
      plot_ly(mtcars, x = ~wt, y = ~mpg,
              type   = "scatter",
              mode   = "markers",
              marker = list(size = 10, opacity = 0.7),
              text   = ~paste("차종:", rownames(mtcars),
                              "<br>무게:", wt,
                              "<br>연비:", mpg),
              hoverinfo = "text") %>%
        layout(title = "무게 vs 연비",
               xaxis = list(title = "무게(ton)"),
               yaxis = list(title = "연비(mpg)"))

    } else {
      plot_ly(x = mtcars$mpg,
              type = "histogram",
              nbinsx = 15,
              marker = list(color = "#3498db",
                            line  = list(color = "white", width = 1))) %>%
        layout(title = "연비 분포",
               xaxis = list(title = "연비(mpg)"),
               yaxis = list(title = "빈도"))
    }
  })
}

shinyApp(ui, server)

줌과 선택 기능

plotly 차트는 기본적으로 다음 인터랙션이 제공됩니다.

동작 설명
마우스 드래그 사각형 선택 후 줌
더블 클릭 원래 크기로 복귀
범례 항목 클릭 해당 시리즈 보이기/숨기기
범례 항목 더블 클릭 해당 시리즈만 표시
모드바 아이콘 줌인/아웃, 팬, 저장 등

모드바를 숨기거나 특정 버튼만 남길 수 있습니다.

p <- plot_ly(...) %>%
  layout(...) %>%
  config(
    displayModeBar = TRUE,
    modeBarButtonsToRemove = c("lasso2d", "select2d", "autoScale2d"),
    toImageButtonOptions = list(
      format   = "png",
      filename = "my_chart",
      width    = 1200,
      height   = 800
    )
  )

애니메이션 차트

frame 인수로 프레임을 지정하면 애니메이션 차트를 만들 수 있습니다.

library(shiny)
library(plotly)

# gapminder 스타일 데이터 생성
set.seed(42)
years   <- 2000:2020
countries <- c("Korea", "Japan", "China", "USA")
anim_data <- expand.grid(year = years, country = countries) %>%
  dplyr::mutate(
    gdp  = runif(nrow(.), 5000, 60000),
    life = runif(nrow(.), 60, 90)
  )

ui <- fluidPage(
  titlePanel("연도별 변화 애니메이션"),
  plotlyOutput("anim_chart", height = "500px")
)

server <- function(input, output, session) {
  output$anim_chart <- renderPlotly({
    plot_ly(
      data   = anim_data,
      x      = ~gdp,
      y      = ~life,
      color  = ~country,
      frame  = ~year,          # 애니메이션 프레임
      text   = ~country,
      type   = "scatter",
      mode   = "markers",
      marker = list(size = 15, opacity = 0.8)
    ) %>%
      layout(
        xaxis = list(title = "1인당 GDP"),
        yaxis = list(title = "기대수명")
      ) %>%
      animation_opts(
        frame      = 800,   # 프레임 간 시간(ms)
        redraw     = FALSE,
        transition = 500
      ) %>%
      animation_button(label = "재생")
  })
}

shinyApp(ui, server)

plotly 이벤트 처리

plotly 차트에서 클릭한 데이터 포인트 정보를 서버에서 받을 수 있습니다.

ui <- fluidPage(
  plotlyOutput("scatter"),
  verbatimTextOutput("click_info")
)

server <- function(input, output, session) {
  output$scatter <- renderPlotly({
    plot_ly(mtcars, x = ~hp, y = ~mpg,
            type = "scatter", mode = "markers",
            key  = rownames(mtcars),       # 클릭 시 전달할 키
            source = "main_scatter")       # 이벤트 소스 ID
  })

  output$click_info <- renderPrint({
    d <- event_data("plotly_click", source = "main_scatter")
    if (is.null(d)) {
      cat("차트의 점을 클릭하세요.\n")
    } else {
      cat("클릭한 점:\n")
      cat("  마력(hp):", d$x, "\n")
      cat("  연비(mpg):", d$y, "\n")
      cat("  차종:", d$key, "\n")
    }
  })
}

shinyApp(ui, server)

event_data()로 받을 수 있는 이벤트는 "plotly_click", "plotly_hover", "plotly_selected", "plotly_relayout" 등이 있습니다.