In this guide, you will combine yahoofinancer with Shiny to build a live market
dashboard: a web application that shows real-time quote cards for any
ticker, its position within the 52-week range, and an interactive price
history chart driven by period and interval selectors. No prior Shiny
experience is required—every reactive concept used here is explained as
it appears.
Every Shiny app has two halves:
| Piece | Role | In this dashboard |
|---|---|---|
ui |
Declares layout and widgets | Ticker picker, period/interval selects, quote cards, chart area |
server |
Holds the logic that reacts to inputs | Fetches quotes and price history, renders text and plots |
The bridge between them is reactivity: when the user
changes a widget, expressions that depend on it re-execute
automatically. We will use eventReactive() so that network
calls to Yahoo Finance fire only when the user presses the
Refresh button—polite to both the user and the API.
The UI below uses sidebarLayout(): controls on the left,
outputs on the right. Note how named vectors in choices
display friendly labels (e.g., “1 Month”) while passing compact
API values ("1mo") to
yf_download_prices().
ui <- fluidPage(
title = "Live Market Dashboard",
titlePanel("Live Market Dashboard"),
sidebarLayout(
sidebarPanel(
width = 3,
selectInput(
inputId = "symbol",
label = "Ticker",
choices = c("AAPL", "MSFT", "GOOG", "AMZN", "NVDA", "META")
),
selectInput(
inputId = "period",
label = "History length",
choices = c(
"1 Month" = "1mo", "3 Months" = "3mo",
"6 Months" = "6mo", "1 Year" = "1y",
"2 Years" = "2y", "5 Years" = "5y"
),
selected = "6mo"
),
selectInput(
inputId = "interval",
label = "Bar interval",
choices = c("Daily" = "1d", "Weekly" = "1wk", "Monthly" = "1mo")
),
actionButton("refresh", "Refresh data", class = "btn-primary")
),
mainPanel(
width = 9,
# Quote cards row
fluidRow(
column(4, wellPanel(
h4(textOutput("price_card")),
p(uiOutput("change_card"), style = "margin-bottom: 0;")
)),
column(4, wellPanel(
h4("52-Week Range"),
p(textOutput("range_card"), style = "margin-bottom: 0;")
)),
column(4, wellPanel(
h4("Volume"),
p(textOutput("volume_card"), style = "margin-bottom: 0;")
))
),
# Price history chart
plotOutput("price_chart", height = "360px")
)
)
)The server fetches two datasets per refresh:
yf_get_market_stats() feeding
the quote cards.yf_download_prices() feeding the
chart.Both are wrapped in eventReactive(input$refresh, ...),
so each press of the button triggers exactly one pair of requests.
server <- function(input, output, session) {
stats <- eventReactive(input$refresh, {
yf_get_market_stats(input$symbol)
})
prices <- eventReactive(input$refresh, {
yf_download_prices(
tickers = input$symbol,
period = input$period,
interval = input$interval
)
})
# ... output renderers added in sections 4 and 5 ...
}A snapshot looks like this:
yf_get_market_stats("AAPL")
#> # A tibble: 1 × 7
#> symbol regular_market_price fifty_two_week_high fifty_two_week_low
#> <chr> <dbl> <dbl> <dbl>
#> 1 AAPL 232. 237. 169.
#> # i 3 more variables: regular_market_volume <dbl>, previous_close <dbl>,
#> # currency <chr>(Note: Values are illustrative; live results reflect the latest available session.)
The cards read from the stats() reactive.
price_card, range_card, and
volume_card use renderText(), while
change_card uses renderUI() to color the
percentage move green or red based on direction. The range card places
the current price within the 52-week band.
output$price_card <- renderText({
s <- req(stats())
req(nrow(s) > 0)
sprintf("%s · %s", s$symbol, dollar(s$regular_market_price))
})
output$change_card <- renderUI({
s <- req(stats())
req(nrow(s) > 0)
pct <- 100 * (s$regular_market_price - s$previous_close) / s$previous_close
color <- if (pct >= 0) "#2e7d32" else "#c62828"
tags$span(
style = sprintf("color:%s", color),
sprintf("%+.2f%% vs previous close", pct)
)
})
output$range_card <- renderText({
s <- req(stats())
req(nrow(s) > 0)
pos <- 100 * (s$regular_market_price - s$fifty_two_week_low) /
(s$fifty_two_week_high - s$fifty_two_week_low)
sprintf(
"%s — %s\n(%s of 52-week range)",
dollar(s$fifty_two_week_low), dollar(s$fifty_two_week_high),
percent(pos / 100, accuracy = 1)
)
})
output$volume_card <- renderText({
s <- req(stats())
req(nrow(s) > 0)
label_number(scale_cut = scales::cut_short_scale())(s$regular_market_volume)
})Two defensive details worth keeping:
req(nrow(s) > 0) silently suspends rendering until a
valid dataset arrives—since yf_get_market_stats() returns
an empty tibble on network failure or invalid ticker, this check
prevents empty rows from flashing errors into the UI.scales::percent() with explicit
accuracy, so values like 12.34567% never leak
into the UI.The chart plots closing prices over time with dashed reference lines
at the 52-week extremes. Because date arrives as a
POSIXct datetime, the axis works unchanged for daily,
weekly, and monthly bars alike.
output$price_chart <- renderPlot({
df <- req(prices())
req(nrow(df) > 0)
s <- req(stats())
req(nrow(s) > 0)
ggplot(df, aes(x = date, y = close)) +
geom_line(color = "#1f77b4", linewidth = 0.9) +
geom_hline(
yintercept = s$fifty_two_week_high,
linetype = "dashed", color = "#2e7d32"
) +
geom_hline(
yintercept = s$fifty_two_week_low,
linetype = "dashed", color = "#c62828"
) +
scale_y_continuous(labels = dollar_format()) +
labs(
title = paste(s$symbol, "Price History"),
subtitle = "Dashed lines mark 52-week high and low",
x = NULL,
y = "Price (USD)",
caption = "Source: Yahoo Finance via yahoofinancer"
) +
theme_minimal(base_size = 12) +
theme(plot.title = element_text(face = "bold"))
})Here is the complete, self-contained app in a single copy-pasteable
script. Save it as app.R and run
shiny::runApp().
library(yahoofinancer)
library(shiny)
library(dplyr)
library(ggplot2)
library(scales)
ui <- fluidPage(
titlePanel("Live Market Dashboard"),
sidebarLayout(
sidebarPanel(
width = 3,
selectInput("symbol", "Ticker",
choices = c("AAPL", "MSFT", "GOOG", "AMZN", "NVDA", "META")
),
selectInput("period", "History length",
choices = c(
"1 Month" = "1mo", "3 Months" = "3mo",
"6 Months" = "6mo", "1 Year" = "1y",
"2 Years" = "2y", "5 Years" = "5y"
),
selected = "6mo"
),
selectInput("interval", "Bar interval",
choices = c(
"Daily" = "1d", "Weekly" = "1wk",
"Monthly" = "1mo"
)
),
actionButton("refresh", "Refresh data", class = "btn-primary")
),
mainPanel(
width = 9,
fluidRow(
column(4, wellPanel(
h4(textOutput("price_card")),
p(uiOutput("change_card"), style = "margin-bottom: 0;")
)),
column(4, wellPanel(
h4("52-Week Range"),
p(textOutput("range_card"), style = "margin-bottom: 0;")
)),
column(4, wellPanel(
h4("Volume"),
p(textOutput("volume_card"), style = "margin-bottom: 0;")
))
),
plotOutput("price_chart", height = "360px")
)
)
)
server <- function(input, output, session) {
stats <- eventReactive(input$refresh, yf_get_market_stats(input$symbol))
prices <- eventReactive(input$refresh, {
yf_download_prices(input$symbol,
period = input$period,
interval = input$interval
)
})
output$price_card <- renderText({
s <- req(stats())
req(nrow(s) > 0)
sprintf("%s · %s", s$symbol, dollar(s$regular_market_price))
})
output$change_card <- renderUI({
s <- req(stats())
req(nrow(s) > 0)
pct <- 100 * (s$regular_market_price - s$previous_close) / s$previous_close
color <- if (pct >= 0) "#2e7d32" else "#c62828"
tags$span(
style = sprintf("color:%s", color),
sprintf("%+.2f%% vs previous close", pct)
)
})
output$range_card <- renderText({
s <- req(stats())
req(nrow(s) > 0)
pos <- 100 * (s$regular_market_price - s$fifty_two_week_low) /
(s$fifty_two_week_high - s$fifty_two_week_low)
sprintf(
"%s — %s\n(%s of range)",
dollar(s$fifty_two_week_low),
dollar(s$fifty_two_week_high),
percent(pos / 100, accuracy = 1)
)
})
output$volume_card <- renderText({
s <- req(stats())
req(nrow(s) > 0)
label_number(scale_cut = cut_short_scale())(s$regular_market_volume)
})
output$price_chart <- renderPlot({
df <- req(prices())
req(nrow(df) > 0)
s <- req(stats())
req(nrow(s) > 0)
ggplot(df, aes(x = date, y = close)) +
geom_line(color = "#1f77b4", linewidth = 0.9) +
geom_hline(
yintercept = s$fifty_two_week_high,
linetype = "dashed", color = "#2e7d32"
) +
geom_hline(
yintercept = s$fifty_two_week_low,
linetype = "dashed", color = "#c62828"
) +
scale_y_continuous(labels = dollar_format()) +
labs(title = paste(s$symbol, "Price History"), x = NULL, y = "Price (USD)") +
theme_minimal(base_size = 12) +
theme(plot.title = element_text(face = "bold"))
})
}
shinyApp(ui, server)(Note: Remember to press Refresh data after launching—the app deliberately makes no network calls until asked.)
In this guide, you learned how to:
fluidPage UI with a server function connected
by reactive IDs.eventReactive()
so Yahoo Finance is queried only when the user requests new data.yf_get_market_stats() snapshots into formatted quote cards
with scales labeling.yf_download_prices() from period/interval widgets and
overlay 52-week reference levels.get_trending("US") or get_trending("GB").observe({ invalidateLater(60000); shinyjs::click("refresh") })
(with the shinyjs package) to poll every minute during
market hours.Tickers class
(?Tickers)—Tickers$new(c("AAPL", "MSFT"))$get_history(period = "6mo")
returns one long tibble; sort chronologically inside groups with
arrange(date, .by_group = TRUE) before plotting one line
per symbol.validate() before fetching.vignette("cookbook", package = "yahoofinancer").