// Candlestick (OHLC) chart.
///|
/// Render a candlestick chart as a standalone SVG document string. Each entry
/// of `data` is `(label, open, high, low, close)`; the thin wick spans
/// low..high and the body spans open..close. Rising candles (close >= open)
/// use `up_color`, falling ones `down_color`. `title`, `width`, `height` and
/// `theme` are optional.
pub fn candlestick_chart(
data : Array[(String, Double, Double, Double, Double)],
up_color? : String = "#59a14f",
down_color? : String = "#e15759",
title? : String = "",
width? : Double = 520.0,
height? : Double = 340.0,
theme? : Theme = Theme::light(),
) -> String {
let left = 48.0
let right = 16.0
let top = if title == "" { 16.0 } else { 40.0 }
let bottom = 40.0
let plot_w = width - left - right
let plot_h = height - top - bottom
// The value domain spans every wick.
let lows = data.map(fn(d) { d.3 })
let highs = data.map(fn(d) { d.2 })
let (lo_raw, _) = extent(lows)
let (_, hi_raw) = extent(highs)
let (axis_lo, axis_hi, ticks) = nice_ticks(lo_raw, hi_raw, 5)
let n = data.length()
let body = StringBuilder::new()
body.write_string(background_rect(theme, width, height))
body.write_string(
y_axis(theme, left, top, plot_w, plot_h, axis_lo, axis_hi, ticks),
)
if n > 0 {
let slot = plot_w / n.to_double()
let body_w = slot * 0.55
let sy = fn(v : Double) -> Double {
scale_linear(v, axis_lo, axis_hi, top + plot_h, top)
}
for i in 0..= open { up_color } else { down_color }
// Wick from low to high.
body.write_string(
elem("line", [
("x1", num(cx)),
("y1", num(sy(low))),
("x2", num(cx)),
("y2", num(sy(high))),
("stroke", color),
("stroke-width", "1.5"),
]),
)
// Body between open and close; keep at least 1px so dojis stay visible.
let top_v = if open > close { open } else { close }
let bot_v = if open > close { close } else { open }
let y = sy(top_v)
let h = {
let hh = sy(bot_v) - sy(top_v)
if hh < 1.0 {
1.0
} else {
hh
}
}
body.write_string(
elem("rect", [
("x", num(cx - body_w / 2.0)),
("y", num(y)),
("width", num(body_w)),
("height", num(h)),
("fill", color),
("rx", "1"),
]),
)
// Category label below the plot area.
body.write_string(label(cx, top + plot_h + 16.0, name, fill=theme.text))
}
}
if title != "" {
body.write_string(
label(width / 2.0, 24.0, title, size=16, weight="bold", fill=theme.title),
)
}
document(width, height, body.to_string())
}