// XY line chart.
///|
/// Render a line chart connecting a series of `(x, y)` points as a standalone
/// SVG document string. The value axis auto-fits the data with nicely rounded
/// gridlines; when all y-values are positive the baseline is anchored at zero.
/// `errors[i]` (optional) draws a symmetric ±error bar on point `i`; `smooth`
/// replaces straight segments with a Catmull-Rom curve; `x_title` / `y_title`
/// add axis captions. `title`, `width`, `height` and `theme` are optional.
pub fn line_chart(
data : Array[(Double, Double)],
errors? : Array[Double] = [],
smooth? : Bool = false,
x_title? : String = "",
y_title? : String = "",
title? : String = "",
width? : Double = 480.0,
height? : Double = 320.0,
theme? : Theme = Theme::light(),
) -> String {
let left = if y_title == "" { 48.0 } else { 64.0 }
let right = 16.0
let top = if title == "" { 16.0 } else { 40.0 }
let bottom = if x_title == "" { 40.0 } else { 56.0 }
let plot_w = width - left - right
let plot_h = height - top - bottom
let xs = data.map(fn(p) { p.0 })
let ys = data.map(fn(p) { p.1 })
let (xlo_raw, xhi_raw) = extent(xs)
let (ylo_raw, yhi) = extent_with_errors(ys, errors)
// Anchor the baseline at zero for the common all-positive case.
let ylo = if ylo_raw > 0.0 { 0.0 } else { ylo_raw }
let (axis_lo, axis_hi, ticks) = nice_ticks(ylo, yhi, 5)
// The x domain gets nicely rounded ticks too.
let (xlo, xhi, x_ticks) = nice_ticks(xlo_raw, xhi_raw, 6)
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),
)
body.write_string(x_axis(theme, left, top, plot_w, plot_h, xlo, xhi, x_ticks))
let n = data.length()
if n > 0 {
let color = theme.color_at(0)
let pts = StringBuilder::new()
let px_arr : Array[Double] = []
let py_arr : Array[Double] = []
let coords : Array[(Double, Double)] = []
for i in 0.. 0 {
pts.write_string(" ")
}
pts.write_string(num(px) + "," + num(py))
}
if smooth {
body.write_string(
elem("path", [
("d", smooth_path(coords)),
("fill", "none"),
("stroke", color),
("stroke-width", "2"),
("stroke-linejoin", "round"),
("stroke-linecap", "round"),
]),
)
} else {
body.write_string(
elem("polyline", [
("points", pts.to_string()),
("fill", "none"),
("stroke", color),
("stroke-width", "2"),
("stroke-linejoin", "round"),
("stroke-linecap", "round"),
]),
)
}
// Error bars first so the markers sit on top of them.
for i in 0.. 0.0 {
let (_, y) = data[i]
body.write_string(
error_bar(
px_arr[i],
scale_linear(y - e, axis_lo, axis_hi, top + plot_h, top),
scale_linear(y + e, axis_lo, axis_hi, top + plot_h, top),
8.0,
color,
),
)
}
}
for i in 0.. String {
let left = 48.0
let right = 16.0
let title_h = if title == "" { 8.0 } else { 30.0 }
let legend_h = 22.0
let top = title_h + legend_h
let bottom = 40.0
let plot_w = width - left - right
let plot_h = height - top - bottom
// Domains span every point across all series.
let all_x : Array[Double] = []
let all_y : Array[Double] = []
for s in series {
for p in s.1 {
all_x.push(p.0)
all_y.push(p.1)
}
}
let (xlo_raw, xhi_raw) = extent(all_x)
let (ylo_raw, yhi) = extent(all_y)
let ylo = if ylo_raw > 0.0 { 0.0 } else { ylo_raw }
let (axis_lo, axis_hi, ticks) = nice_ticks(ylo, yhi, 5)
let (xlo, xhi, x_ticks) = nice_ticks(xlo_raw, xhi_raw, 6)
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),
)
body.write_string(x_axis(theme, left, top, plot_w, plot_h, xlo, xhi, x_ticks))
for i in 0.. 0 {
pts.write_string(" ")
}
pts.write_string(num(px) + "," + num(py))
}
body.write_string(
elem("polyline", [
("points", pts.to_string()),
("fill", "none"),
("stroke", color),
("stroke-width", "2"),
("stroke-linejoin", "round"),
("stroke-linecap", "round"),
]),
)
for c in coords {
body.write_string(
elem("circle", [
("cx", num(c.0)),
("cy", num(c.1)),
("r", "3"),
("fill", color),
]),
)
}
}
// Legend of series names.
let legend_items : Array[(String, String)] = []
for i in 0..