Ichimoku Cloud Technical Indicator Chart — ggplot2

Displays the Ichimoku Kinko Hyo ("one glance equilibrium chart") overlay on a candlestick price chart. The indicator plots five lines — Tenkan-sen (conversion), Kijun-sen (base), Senkou Span A, Senkou Span B, and Chikou Span (lagging) — with the area between Senkou Span A and B filled as the "Kumo" (cloud). The cloud color changes depending on which span is on top, providing an at-a-glance view of trend direction, momentum, and support/resistance zones.

Ichimoku Cloud Technical Indicator Chart rendered with ggplot2

Renders

R source (ggplot2)

#' anyplot.ai
#' indicator-ichimoku: Ichimoku Cloud Technical Indicator Chart
#' Library: ggplot2 3.5.1 | R 4.4.1
#' Quality: 88/100 | Created: 2026-06-08

library(ggplot2)
library(dplyr)
library(tidyr)
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"
INK_MUTED   <- if (THEME == "light") "#6B6A63" else "#A8A79F"

# Imprint palette
IMPRINT_PALETTE <- c(
    "#009E73",  # 1 — brand green
    "#C475FD",  # 2 — lavender
    "#4467A3",  # 3 — blue
    "#BD8233",  # 4 — ochre
    "#AE3030",  # 5 — matte red
    "#2ABCCD",  # 6 — cyan
    "#954477",  # 7 — rose
    "#99B314"   # 8 — lime
)

# Data — synthetic Nikkei 225 daily OHLC, ~160 trading days
n_periods <- 200
dates     <- seq.Date(as.Date("2023-01-03"), by = "day", length.out = n_periods)
dates     <- dates[weekdays(dates) %in% c("Monday", "Tuesday", "Wednesday", "Thursday", "Friday")]
dates     <- dates[seq_len(min(160, length(dates)))]
n         <- length(dates)

log_returns  <- cumsum(rnorm(n, mean = 0.0005, sd = 0.012))
close_prices <- 150 * exp(log_returns)
daily_range  <- abs(rnorm(n, mean = 0.008, sd = 0.004)) * close_prices
open_prices  <- close_prices - rnorm(n, 0, 0.003) * close_prices
high_prices  <- pmax(open_prices, close_prices) + daily_range * 0.6
low_prices   <- pmin(open_prices, close_prices) - daily_range * 0.4

# Ichimoku helpers (standard 9 / 26 / 52 parameters)
roll_high <- function(x, k) {
    sapply(seq_along(x), function(i) if (i < k) NA else max(x[(i - k + 1):i]))
}
roll_low <- function(x, k) {
    sapply(seq_along(x), function(i) if (i < k) NA else min(x[(i - k + 1):i]))
}

tenkan_sen    <- (roll_high(high_prices, 9)  + roll_low(low_prices, 9))  / 2
kijun_sen     <- (roll_high(high_prices, 26) + roll_low(low_prices, 26)) / 2
span_a_raw    <- (tenkan_sen + kijun_sen) / 2
span_b_raw    <- (roll_high(high_prices, 52) + roll_low(low_prices, 52)) / 2
senkou_span_a <- c(rep(NA, 26), span_a_raw)[seq_len(n)]
senkou_span_b <- c(rep(NA, 26), span_b_raw)[seq_len(n)]
chikou_span   <- c(close_prices[27:n], rep(NA, 26))

df <- data.frame(
    date          = dates,
    open          = open_prices,
    high          = high_prices,
    low           = low_prices,
    close         = close_prices,
    tenkan_sen    = tenkan_sen,
    kijun_sen     = kijun_sen,
    senkou_span_a = senkou_span_a,
    senkou_span_b = senkou_span_b,
    chikou_span   = chikou_span
)

df$up     <- df$close >= df$open
df$up_chr <- as.character(df$up)  # "TRUE"/"FALSE" strings for color scale

CANDLE_UP   <- IMPRINT_PALETTE[1]
CANDLE_DOWN <- IMPRINT_PALETTE[5]

# Cloud data split by bullish/bearish for two-color fill
df_cloud <- df |>
    filter(!is.na(senkou_span_a) & !is.na(senkou_span_b)) |>
    mutate(
        upper      = pmax(senkou_span_a, senkou_span_b),
        lower      = pmin(senkou_span_a, senkou_span_b),
        cloud_type = if_else(senkou_span_a >= senkou_span_b, "Bullish Cloud", "Bearish Cloud")
    )

# Indicator lines in long format — enables merged color+linetype legend
df_lines <- df |>
    pivot_longer(
        cols      = c(tenkan_sen, kijun_sen, chikou_span, senkou_span_a, senkou_span_b),
        names_to  = "indicator",
        values_to = "value"
    ) |>
    mutate(
        indicator = factor(
            indicator,
            levels = c("tenkan_sen", "kijun_sen", "chikou_span", "senkou_span_a", "senkou_span_b"),
            labels = c("Tenkan-sen", "Kijun-sen", "Chikou Span", "Senkou Span A", "Senkou Span B")
        )
    )

# Scale definitions
IND_BREAKS <- c("Tenkan-sen", "Kijun-sen", "Chikou Span", "Senkou Span A", "Senkou Span B")

# Combined color: candle wicks ("TRUE"/"FALSE") + indicator lines — wicks hidden via breaks
ALL_COLORS <- c(
    "TRUE"          = CANDLE_UP,
    "FALSE"         = CANDLE_DOWN,
    "Tenkan-sen"    = IMPRINT_PALETTE[1],
    "Kijun-sen"     = IMPRINT_PALETTE[3],
    "Chikou Span"   = IMPRINT_PALETTE[4],
    "Senkou Span A" = IMPRINT_PALETTE[1],
    "Senkou Span B" = IMPRINT_PALETTE[5]
)

# Combined fill: candle bodies + cloud — bodies hidden via breaks
ALL_FILLS <- c(
    "TRUE"          = CANDLE_UP,
    "FALSE"         = CANDLE_DOWN,
    "Bullish Cloud" = IMPRINT_PALETTE[1],
    "Bearish Cloud" = IMPRINT_PALETTE[5]
)

LINE_TYPES <- c(
    "Tenkan-sen"    = "solid",
    "Kijun-sen"     = "solid",
    "Chikou Span"   = "dotted",
    "Senkou Span A" = "dashed",
    "Senkou Span B" = "dashed"
)

# Find first Kumo twist (cloud color flip) for annotation
cloud_flips <- df_cloud |>
    arrange(date) |>
    mutate(prev_type = lag(cloud_type)) |>
    filter(!is.na(prev_type), cloud_type != prev_type)

# Title
title_str   <- "Nikkei 225 · indicator-ichimoku · r · ggplot2 · anyplot.ai"
n_title     <- nchar(title_str)
title_fs    <- max(8, round(12 * 67 / n_title))
price_range <- range(df$low, df$high, na.rm = TRUE)

p <- ggplot(df, aes(x = date)) +

    # Kumo (cloud) — bearish layer first, bullish on top; fill → Cloud legend
    geom_ribbon(
        data  = filter(df_cloud, cloud_type == "Bearish Cloud"),
        aes(ymin = lower, ymax = upper, fill = cloud_type),
        alpha = 0.25
    ) +
    geom_ribbon(
        data  = filter(df_cloud, cloud_type == "Bullish Cloud"),
        aes(ymin = lower, ymax = upper, fill = cloud_type),
        alpha = 0.25
    ) +

    # Candlestick wicks — color mapped for correct coloring, excluded from legend
    geom_segment(
        aes(xend = date, y = low, yend = high, color = up_chr),
        linewidth = 0.4
    ) +

    # Candlestick bodies — fill mapped for correct coloring, excluded from legend
    geom_rect(
        aes(
            xmin = date - 0.3, xmax = date + 0.3,
            ymin = pmin(open, close), ymax = pmax(open, close),
            fill = up_chr
        ),
        color = NA
    ) +

    # Tenkan-sen + Kijun-sen (thick solid momentum lines)
    geom_line(
        data      = filter(df_lines, indicator %in% c("Tenkan-sen", "Kijun-sen")),
        aes(y = value, color = indicator, linetype = indicator),
        linewidth = 0.9, na.rm = TRUE
    ) +
    # Chikou Span — thicker dotted line for visibility through candlestick forest
    geom_line(
        data      = filter(df_lines, indicator == "Chikou Span"),
        aes(y = value, color = indicator, linetype = indicator),
        linewidth = 1.0, na.rm = TRUE
    ) +
    # Senkou Span A + B (thin dashed cloud boundaries)
    geom_line(
        data      = filter(df_lines, indicator %in% c("Senkou Span A", "Senkou Span B")),
        aes(y = value, color = indicator, linetype = indicator),
        linewidth = 0.5, na.rm = TRUE
    ) +

    # Color scale: only indicator lines appear in "Lines" legend
    scale_color_manual(
        name   = "Lines",
        values = ALL_COLORS,
        breaks = IND_BREAKS
    ) +
    # Fill scale: only cloud fills appear in "Cloud" legend
    scale_fill_manual(
        name   = "Cloud",
        values = ALL_FILLS,
        breaks = c("Bullish Cloud", "Bearish Cloud")
    ) +
    # Linetype scale merged with color into one "Lines" legend box
    scale_linetype_manual(
        name   = "Lines",
        values = LINE_TYPES,
        breaks = IND_BREAKS
    ) +

    scale_x_date(
        date_breaks = "1 month",
        date_labels = "%b %Y",
        expand      = expansion(mult = 0.01)
    ) +
    scale_y_continuous(
        labels = label_dollar(),
        expand = expansion(mult = c(0.03, 0.10))
    ) +

    # Cloud legend keys show semi-transparent swatches (no border)
    guides(fill = guide_legend(override.aes = list(alpha = 0.3, color = NA))) +

    labs(
        title = title_str,
        x     = NULL,
        y     = "Price (USD)"
    ) +

    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 = INK_MUTED, linewidth = 0.2),
        panel.grid.minor  = element_blank(),
        panel.border      = element_blank(),
        axis.title.y      = element_text(color = INK,      size = 10),
        axis.text         = element_text(color = INK_SOFT, size = 8),
        axis.text.x       = element_text(angle = 30, hjust = 1),
        axis.line         = element_line(color = INK_SOFT, linewidth = 0.4),
        axis.ticks        = element_line(color = INK_SOFT, linewidth = 0.3),
        plot.title        = element_text(color = INK, size = title_fs, face = "bold",
                                         margin = margin(b = 8)),
        plot.margin       = margin(12, 16, 10, 12),
        legend.background = element_rect(fill = ELEVATED_BG, color = INK_SOFT, linewidth = 0.3),
        legend.text       = element_text(color = INK_SOFT, size = 8),
        legend.title      = element_text(color = INK,      size = 9, face = "bold"),
        legend.key.size   = unit(1.0, "lines"),
        legend.position   = "right",
        legend.margin     = margin(4, 6, 4, 6)
    )

# Annotate first Kumo twist (cloud color flip = trend-change signal)
if (nrow(cloud_flips) > 0) {
    flip_date  <- cloud_flips$date[1]
    flip_upper <- df_cloud |> filter(date == flip_date) |> pull(upper)
    if (length(flip_upper) == 1 && !is.na(flip_upper)) {
        rng      <- diff(price_range)
        annot_y  <- flip_upper + rng * 0.07
        arrow_y1 <- annot_y   - rng * 0.015
        arrow_y2 <- flip_upper + rng * 0.015
        p <- p +
            annotate("text",
                     x = flip_date, y = annot_y,
                     label = "Kumo twist", color = INK_SOFT,
                     size = 2.5, hjust = 0.5) +
            annotate("segment",
                     x = flip_date, xend = flip_date,
                     y = arrow_y1, yend = arrow_y2,
                     color = INK_SOFT, linewidth = 0.3,
                     arrow = arrow(length = unit(0.08, "cm"), type = "open"))
    }
}

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

Retrieve this implementation

Runnable source as JSON, for any HTTP client: https://api.anyplot.ai/specs/indicator-ichimoku/ggplot2/code. Any spec id and library id listed in llms-full.txt fit the same URL shape; every URL below is complete and callable.

{
  "spec_id": "indicator-ichimoku",
  "language": "r",
  "library": "ggplot2",
  "page": "https://anyplot.ai/indicator-ichimoku/r/ggplot2",
  "hub": "https://anyplot.ai/indicator-ichimoku",
  "code_json": "https://api.anyplot.ai/specs/indicator-ichimoku/ggplot2/code",
  "spec_json": "https://api.anyplot.ai/specs/indicator-ichimoku",
  "render_light_png": "https://storage.googleapis.com/anyplot-images/plots/indicator-ichimoku/r/ggplot2/plot-light.png",
  "render_dark_png": "https://storage.googleapis.com/anyplot-images/plots/indicator-ichimoku/r/ggplot2/plot-dark.png",
  "quality_score": 88.0,
  "license": "MIT",
  "guide": "https://anyplot.ai/llms.txt"
}

Part of Ichimoku Cloud Technical Indicator Chart on anyplot.ai.

Other implementations