// Vertical bar chart.

///|
/// Render a vertical bar chart as a standalone SVG document string.
///
/// `data` is a list of `(label, value)` pairs; negative values draw downward
/// from the zero baseline. `errors[i]` (optional) draws a symmetric ±error bar
/// on bar `i`. `title`, `width`, `height` and `theme` are optional.
pub fn bar_chart(
  data : Array[(String, Double)],
  errors? : Array[Double] = [],
  title? : String = "",
  width? : Double = 480.0,
  height? : Double = 320.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 = 48.0
  let plot_w = width - left - right
  let plot_h = height - top - bottom
  // The value range (widened by any error bars) sets the vertical scale;
  // always include zero so bars have a meaningful baseline, and guard the
  // all-zero case.
  let values = data.map(fn(d) { d.1 })
  let (raw_lo, raw_hi) = extent_with_errors(values, errors)
  let lo = if raw_lo > 0.0 { 0.0 } else { raw_lo }
  let hi = if raw_hi < 0.0 { 0.0 } else { raw_hi }
  let hi = if lo == 0.0 && hi == 0.0 { 1.0 } else { hi }
  let (axis_lo, axis_hi, ticks) = nice_ticks(lo, hi, 5)
  let n = data.length()
  let body = StringBuilder::new()
  body.write_string(background_rect(theme, width, height))
  // Gridlines and y-axis labels sit behind the bars.
  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 bar_w = slot * 0.62
    let zero_y = scale_linear(0.0, axis_lo, axis_hi, top + plot_h, top)
    for i in 0..= 0.0 { val_y } else { zero_y }
      let bar_h = if value >= 0.0 { zero_y - val_y } else { val_y - zero_y }
      // The bar itself, growing up or down from the zero line.
      body.write_string(
        elem("rect", [
          ("x", num(x)),
          ("y", num(y)),
          ("width", num(bar_w)),
          ("height", num(bar_h)),
          ("fill", theme.color_at(i)),
          ("rx", "2"),
        ]),
      )
      // Optional ±error bar centered on the bar tip; the value label then
      // moves past the error bar so they never overlap.
      let e = error_at(errors, i)
      if e > 0.0 {
        body.write_string(
          error_bar(
            x + bar_w / 2.0,
            scale_linear(value - e, axis_lo, axis_hi, top + plot_h, top),
            scale_linear(value + e, axis_lo, axis_hi, top + plot_h, top),
            bar_w * 0.4,
            theme.text,
          ),
        )
      }
      // Value printed just beyond the tip of the bar (or its error bar).
      let tip = if value >= 0.0 { value + e } else { value - e }
      let tip_y = scale_linear(tip, axis_lo, axis_hi, top + plot_h, top)
      let label_y = if value >= 0.0 { tip_y - 6.0 } else { tip_y + 14.0 }
      body.write_string(
        label(x + bar_w / 2.0, label_y, num(value), fill=theme.text),
      )
      // Category label below the plot area.
      body.write_string(
        label(x + bar_w / 2.0, 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())
}

///|
/// Render a grouped bar chart: for each category, one bar per series, drawn
/// side by side, with a legend. `categories` names the groups along the x-axis;
/// `series` is a list of `(name, values)` pairs where `values[i]` is the value
/// for category `i`. `title`, `width`, `height` and `theme` are optional.
pub fn bar_chart_grouped(
  categories : Array[String],
  series : Array[(String, Array[Double])],
  title? : String = "",
  width? : Double = 520.0,
  height? : Double = 340.0,
  theme? : Theme = Theme::light(),
) -> 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 = 48.0
  let plot_w = width - left - right
  let plot_h = height - top - bottom
  // Largest value across every series sets the vertical scale.
  let mut raw_max = 0.0
  for s in series {
    for v in s.1 {
      if v > raw_max {
        raw_max = v
      }
    }
  }
  let max = if raw_max <= 0.0 { 1.0 } else { raw_max }
  let (axis_lo, axis_hi, ticks) = nice_ticks(0.0, max, 5)
  let n_cat = categories.length()
  let n_ser = series.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_cat > 0 && n_ser > 0 {
    let slot = plot_w / n_cat.to_double()
    let group_w = slot * 0.7
    let bar_w = group_w / n_ser.to_double()
    for c 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 = 48.0
  let plot_w = width - left - right
  let plot_h = height - top - bottom
  let n_cat = categories.length()
  let n_ser = series.length()
  // The tallest stacked total sets the vertical scale.
  let mut raw_max = 0.0
  for c in 0.. raw_max {
      raw_max = total
    }
  }
  let max = if raw_max <= 0.0 { 1.0 } else { raw_max }
  let (axis_lo, axis_hi, ticks) = nice_ticks(0.0, max, 5)
  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_cat > 0 && n_ser > 0 {
    let slot = plot_w / n_cat.to_double()
    let bar_w = slot * 0.6
    for c in 0.. 0.0 {
          let seg_h = value / axis_hi * plot_h
          let y = top + plot_h - acc / axis_hi * plot_h - seg_h
          body.write_string(
            elem("rect", [
              ("x", num(x)),
              ("y", num(y)),
              ("width", num(bar_w)),
              ("height", num(seg_h)),
              ("fill", theme.color_at(s)),
            ]),
          )
          acc = acc + value
        }
      }
      body.write_string(
        label(
          x + bar_w / 2.0,
          top + plot_h + 16.0,
          categories[c],
          fill=theme.text,
        ),
      )
    }
  }
  let legend_items : Array[(String, String)] = []
  for s in 0..