OHLC Bar Chart — ggplot2

An OHLC (Open-High-Low-Close) bar chart displays financial price data using vertical bars with horizontal tick marks. Each bar shows the price range from high to low as a thin vertical line, with a left tick indicating the opening price and a right tick indicating the closing price. Unlike candlestick charts that use colored bodies, OHLC bars provide a cleaner, less cluttered view favored by technical analysts who prefer to focus on price levels rather than visual patterns.

OHLC Bar Chart rendered with ggplot2

Renders

R source (ggplot2)

#' anyplot.ai
#' ohlc-bar: OHLC Bar Chart
#' Library: ggplot2 3.5.1 | R 4.4.1
#' Quality: 87/100 | Created: 2026-05-17

library(ggplot2)
library(dplyr)
library(ragg)

set.seed(42)

# --- Theme tokens -----------------------------------------------------------
THEME       <- Sys.getenv("ANYPLOT_THEME", "light")
PAGE_BG     <- if (THEME == "light") "#FAF8F1" else "#1A1A17"
INK         <- if (THEME == "light") "#1A1A17" else "#F0EFE8"
INK_SOFT    <- if (THEME == "light") "#4A4A44" else "#B8B7B0"
BORDER_COL  <- if (THEME == "light") "#D0CEC4" else "#3F3D37"
GRID_COL    <- if (THEME == "light") "#E8E6DC" else "#2A2824"
IMPRINT   <- c("#009E73", "#C475FD", "#4467A3", "#BD8233",
                 "#AE3030", "#2ABCCD", "#954477")

# --- Data: Realistic stock prices over 50 trading days ----------------------
dates <- seq(as.Date("2024-01-01"), by = "1 day", length.out = 50)
# Filter to trading days (Mon-Fri)
trading_dates <- dates[weekdays(dates) %in% c("Monday", "Tuesday", "Wednesday", "Thursday", "Friday")]
trading_dates <- trading_dates[1:45]

# Generate realistic OHLC data with trend
n <- length(trading_dates)
returns <- rnorm(n, mean = 0.001, sd = 0.015)
close_prices <- 150 * cumprod(1 + returns)

ohlc_data <- data.frame(
  date = trading_dates,
  open = close_prices + rnorm(n, mean = -0.5, sd = 0.8),
  close = close_prices,
  high = pmax(close_prices, close_prices + abs(rnorm(n, mean = 1.5, sd = 0.6))),
  low = pmin(close_prices, close_prices - abs(rnorm(n, mean = 1.5, sd = 0.6)))
) %>%
  mutate(
    direction = ifelse(close > open, "up", "down"),
    color_val = if_else(direction == "up", "#009E73", "#AE3030"),  # imprint green / red
    x_pos = as.numeric(date),
    volatility = (high - low) / low,
    opacity_val = 0.6 + 0.4 * min(volatility / max(volatility), 1.0)
  )

# --- Build plot segments for high-low and open-close ticks ------------------
# Main high-low vertical lines
hl_segments <- ohlc_data %>%
  mutate(
    y_min = low,
    y_max = high
  ) %>%
  select(x_pos, y_min, y_max, direction, color_val, opacity_val)

# Open tick marks (left side, small horizontal)
open_segments <- ohlc_data %>%
  mutate(
    x_start = x_pos - 1.5,
    x_end = x_pos,
    y = open
  ) %>%
  select(x_start, x_end, y, direction, color_val, opacity_val)

# Close tick marks (right side, small horizontal)
close_segments <- ohlc_data %>%
  mutate(
    x_start = x_pos,
    x_end = x_pos + 1.5,
    y = close
  ) %>%
  select(x_start, x_end, y, direction, color_val, opacity_val)

# Compute moving average for trend visualization
ma_period <- 7
ma_close <- rep(NA, nrow(ohlc_data))
for (i in ma_period:nrow(ohlc_data)) {
  ma_close[i] <- mean(ohlc_data$close[(i - ma_period + 1):i])
}
ohlc_data$ma_close <- ma_close

# --- Create the plot --------------------------------------------------------
p <- ggplot() +
  # Subtle trend line (moving average)
  geom_line(
    data = ohlc_data,
    aes(x = x_pos, y = ma_close),
    color = INK_SOFT,
    linewidth = 0.6,
    linetype = "dotted",
    alpha = 0.5
  ) +
  # High-low vertical lines with opacity variation for volatility
  geom_segment(
    data = hl_segments,
    aes(x = x_pos, xend = x_pos, y = y_min, yend = y_max, color = direction, alpha = opacity_val),
    linewidth = 1.3,
    show.legend = FALSE
  ) +
  # Open tick marks (left)
  geom_segment(
    data = open_segments,
    aes(x = x_start, xend = x_end, y = y, yend = y, color = direction, alpha = opacity_val),
    linewidth = 1.1,
    show.legend = FALSE
  ) +
  # Close tick marks (right)
  geom_segment(
    data = close_segments,
    aes(x = x_start, xend = x_end, y = y, yend = y, color = direction, alpha = opacity_val),
    linewidth = 1.1,
    show.legend = FALSE
  ) +
  scale_color_manual(
    values = c("up" = "#009E73", "down" = "#AE3030")  # imprint semantic anchors
  ) +
  scale_alpha_identity() +
  scale_x_continuous(
    breaks = seq(1, nrow(ohlc_data), by = 5),
    labels = format(ohlc_data$date[seq(1, nrow(ohlc_data), by = 5)], "%b %d"),
    expand = c(0.02, 0)
  ) +
  labs(
    title = "ohlc-bar · ggplot2 · anyplot.ai",
    x = "Date",
    y = "Price ($)"
  ) +
  theme_minimal(base_size = 14) +
  theme(
    plot.background     = element_rect(fill = PAGE_BG, color = PAGE_BG),
    panel.background    = element_rect(fill = PAGE_BG, color = NA),
    panel.border        = element_rect(fill = NA, color = BORDER_COL, linewidth = 0.5),
    panel.grid.major.y  = element_line(color = GRID_COL, linewidth = 0.2),
    panel.grid.major.x  = element_blank(),
    panel.grid.minor    = element_blank(),
    axis.ticks          = element_line(color = BORDER_COL, linewidth = 0.3),
    axis.ticks.length   = unit(4, "pt"),
    axis.title          = element_text(color = INK, size = 20, face = "bold"),
    axis.text           = element_text(color = INK_SOFT, size = 16),
    axis.text.x         = element_text(angle = 45, hjust = 1),
    plot.title          = element_text(color = INK, size = 24, face = "bold", margin = margin(b = 12))
  )

# --- Save -------------------------------------------------------------------
ggsave(
  filename = sprintf("plot-%s.png", THEME),
  plot     = p,
  device   = ragg::agg_png,
  width    = 16,
  height   = 9,
  units    = "in",
  dpi      = 300
)

Part of OHLC Bar Chart on anyplot.ai.

Other implementations