// Nightingale rose (polar area) chart: equal-angle sectors whose radius
// encodes value, so area emphasizes larger categories.
///|
/// SVG path for a filled sector (pie slice) from `a0` to `a1` radians at
/// radius `r`, centered at `(cx, cy)`.
fn rose_sector(
cx : Double,
cy : Double,
r : Double,
a0 : Double,
a1 : Double,
) -> String {
let x0 = cx + r * @math.cos(a0)
let y0 = cy + r * @math.sin(a0)
let x1 = cx + r * @math.cos(a1)
let y1 = cy + r * @math.sin(a1)
let large = if a1 - a0 > @math.PI { "1" } else { "0" }
"M " +
num(cx) +
" " +
num(cy) +
" L " +
num(x0) +
" " +
num(y0) +
" A " +
num(r) +
" " +
num(r) +
" 0 " +
large +
" 1 " +
num(x1) +
" " +
num(y1) +
" Z"
}
///|
/// Render a Nightingale rose chart as a standalone SVG document string. Each
/// `(label, value)` gets an equal-angle sector whose radius is proportional to
/// `sqrt(value)` (so sector *area* tracks the value). Concentric guide rings
/// and a legend are drawn. `title`, `width`, `height` and `theme` are optional.
pub fn rose_chart(
data : Array[(String, Double)],
title? : String = "",
width? : Double = 420.0,
height? : Double = 360.0,
theme? : Theme = Theme::light(),
) -> String {
let title_h = if title == "" { 8.0 } else { 30.0 }
let legend_h = 22.0
let top = title_h + legend_h
let n = data.length()
let cx = width / 2.0
let cy = top + (height - top) / 2.0
let radius = {
let by_h = (height - top - 24.0) / 2.0
let by_w = width / 2.0 - 90.0
if by_h < by_w {
by_h
} else {
by_w
}
}
let body = StringBuilder::new()
body.write_string(background_rect(theme, width, height))
if n > 0 {
let mut vmax = 0.0
for d in data {
if d.1 > vmax {
vmax = d.1
}
}
if vmax <= 0.0 {
vmax = 1.0
}
// Concentric guide rings at quarter steps.
for k in 1..<=4 {
body.write_string(
elem("circle", [
("cx", num(cx)),
("cy", num(cy)),
("r", num(radius * k.to_double() / 4.0)),
("fill", "none"),
("stroke", theme.grid),
("stroke-width", "1"),
]),
)
}
let step = 2.0 * @math.PI / n.to_double()
for i in 0.. 0.0 { data[i].1 } else { 0.0 }
let rr = radius * (v / vmax).sqrt()
let a0 = -@math.PI / 2.0 + step * i.to_double()
let a1 = a0 + step
body.write_string(
elem("path", [
("d", rose_sector(cx, cy, rr, a0, a1)),
("fill", theme.color_at(i)),
("fill-opacity", "0.8"),
(
"stroke",
if theme.background == "" {
"#ffffff"
} else {
theme.background
},
),
("stroke-width", "1"),
]),
)
}
// Legend along the bottom-left.
let legend_items : Array[(String, String)] = []
for i in 0..