Order Book Depth Chart — ggplot2

A market depth chart visualizes a snapshot of an exchange order book as two cumulative step areas around the mid price. Cumulative bid volume rises as a green step area to the left of the mid price (summing buy orders from the best bid downward), while cumulative ask volume rises as a red step area to the right (summing sell orders from the best ask upward). The bid-ask spread appears as a gap at the center, with price on the x-axis and cumulative quantity on the y-axis. This is the iconic chart found in nearly every trading and crypto exchange UI, revealing liquidity, support/resistance walls, and the imbalance between buying and selling pressure.

Order Book Depth Chart rendered with ggplot2

R source (ggplot2)

#' anyplot.ai
#' depth-order-book: Order Book Depth Chart
#' Library: ggplot2 3.5.1 | R 4.4.1
#' Quality: 86/100 | Created: 2026-06-15

library(ggplot2)
library(scales)
library(ragg)

set.seed(42)

# Theme tokens
THEME       <- Sys.getenv("ANYPLOT_THEME", "light")
PAGE_BG     <- if (THEME == "light") "#FAF8F1" else "#1A1A17"
ELEVATED_BG <- if (THEME == "light") "#FFFDF6" else "#242420"
INK         <- if (THEME == "light") "#1A1A17" else "#F0EFE8"
INK_SOFT    <- if (THEME == "light") "#4A4A44" else "#B8B7B0"

# Imprint palette — semantic: green = bids (buy), red = asks (sell)
BID_COLOR <- "#009E73"  # Imprint position 1 (brand green)
ASK_COLOR <- "#AE3030"  # Imprint position 5 (semantic: sell/loss)
GRID_COL  <- adjustcolor(INK, alpha.f = 0.12)

# Market parameters: BTC/USD snapshot near $60,000
mid_price   <- 60000
half_spread <- 5        # $10 total spread
n_levels    <- 50
tick_size   <- 5        # $5 between price levels

# Price grids — index 1 = best (closest to mid), index 50 = worst
bid_prices <- mid_price - half_spread - (seq_len(n_levels) - 1) * tick_size
ask_prices <- mid_price + half_spread + (seq_len(n_levels) - 1) * tick_size

# Individual order sizes with mild trend (deeper levels accumulate more liquidity)
qty_trend <- 1 + (seq_len(n_levels) - 1) * 0.04
bid_qtys  <- rlnorm(n_levels, meanlog = 1.5, sdlog = 0.5) * qty_trend
ask_qtys  <- rlnorm(n_levels, meanlog = 1.5, sdlog = 0.5) * qty_trend

# Insert liquidity walls — large resting orders that act as support/resistance
bid_qtys[12] <- bid_qtys[12] * 7
ask_qtys[18] <- ask_qtys[18] * 5

# Cumulative volume from mid price outward
bid_cum <- cumsum(bid_qtys)
ask_cum <- cumsum(ask_qtys)

# Build step polygon data — vectorized, no loops
make_step_poly <- function(prices, cums, side_label) {
    n  <- length(prices)
    xs <- rep(prices, each = 2)
    ys <- c(0, rep(cums, each = 2))[seq_len(2 * n)]
    data.frame(
        x    = c(xs, prices[n], prices[1]),
        y    = c(ys, 0, 0),
        side = side_label
    )
}

bid_poly <- make_step_poly(bid_prices, bid_cum, "Bid (Buy)")
ask_poly <- make_step_poly(ask_prices, ask_cum, "Ask (Sell)")
order_book <- rbind(bid_poly, ask_poly)

# Step outline data for geom_step — prepend (best_price, 0) for clean initial vertical
bid_step <- data.frame(
    price   = c(bid_prices[1], bid_prices),
    cum_vol = c(0, bid_cum),
    side    = "Bid (Buy)"
)
ask_step <- data.frame(
    price   = c(ask_prices[1], ask_prices),
    cum_vol = c(0, ask_cum),
    side    = "Ask (Sell)"
)
depth_df <- rbind(bid_step, ask_step)

max_cum    <- max(bid_cum[n_levels], ask_cum[n_levels])
label_text <- sprintf("Mid: $%s\nSpread: $%d",
                      format(mid_price, big.mark = ",", scientific = FALSE),
                      half_spread * 2)

plot_title <- "BTC/USD Order Book · depth-order-book · r · ggplot2 · anyplot.ai"
title_size <- max(8, round(12 * 67 / max(nchar(plot_title), 67)))

p <- ggplot() +
    geom_polygon(
        data  = order_book,
        aes(x = x, y = y, fill = side, group = side),
        color = NA, alpha = 0.22
    ) +
    geom_step(
        data      = depth_df,
        aes(x = price, y = cum_vol, color = side, group = side),
        direction = "hv", linewidth = 0.85
    ) +
    geom_vline(
        xintercept = mid_price,
        linetype = "dashed", color = INK_SOFT, linewidth = 0.5
    ) +
    annotate(
        "label",
        x = mid_price, y = max_cum * 0.88,
        label = label_text, color = INK, size = 3.5,
        hjust = 0.5, lineheight = 1.3,
        fill = ELEVATED_BG, label.size = 0.25
    ) +
    scale_fill_manual(
        name   = NULL,
        values = c("Bid (Buy)" = BID_COLOR, "Ask (Sell)" = ASK_COLOR),
        guide  = guide_legend(override.aes = list(alpha = 0.7))
    ) +
    scale_color_manual(
        name   = NULL,
        values = c("Bid (Buy)" = BID_COLOR, "Ask (Sell)" = ASK_COLOR),
        guide  = "none"
    ) +
    scale_x_continuous(
        labels = label_dollar(accuracy = 1),
        breaks = seq(59750, 60250, by = 100),
        expand = expansion(mult = 0.01)
    ) +
    scale_y_continuous(
        labels = label_comma(accuracy = 1),
        limits = c(0, max_cum * 1.12),
        expand = expansion(mult = c(0, 0))
    ) +
    labs(
        title = plot_title,
        x     = "Price (USD)",
        y     = "Cumulative Volume (BTC)"
    ) +
    theme_minimal(base_size = 8) +
    theme(
        plot.background   = element_rect(fill = PAGE_BG,  color = PAGE_BG),
        panel.background  = element_rect(fill = PAGE_BG,  color = NA),
        panel.grid.major  = element_line(color = GRID_COL, linewidth = 0.3),
        panel.grid.minor  = element_blank(),
        panel.border      = element_blank(),
        axis.line         = element_line(color = INK_SOFT, linewidth = 0.4),
        axis.title        = element_text(color = INK,      size = 10),
        axis.text         = element_text(color = INK_SOFT, size = 8),
        axis.text.x       = element_text(color = INK_SOFT, size = 8,
                                         angle = 30, hjust = 1),
        plot.title        = element_text(color = INK, size = title_size,
                                         face = "bold", margin = margin(b = 8)),
        legend.background = element_rect(fill = ELEVATED_BG, color = INK_SOFT,
                                         linewidth = 0.3),
        legend.text       = element_text(color = INK_SOFT, size = 8),
        legend.key        = element_rect(fill = NA, color = NA),
        plot.margin       = margin(t = 12, r = 16, b = 8, l = 8)
    )

ggsave(
    filename = sprintf("plot-%s.png", THEME),
    plot     = p,
    device   = ragg::agg_png,
    width    = 8,
    height   = 4.5,
    units    = "in",
    dpi      = 400
)

Part of Order Book Depth Chart on anyplot.ai.

Other implementations