// Waterfall chart: signed deltas accumulate left to right.

///|
/// Render a waterfall chart as a standalone SVG document string. Each
/// `(label, delta)` bar floats at the running total, colored `up_color` for
/// gains and `down_color` for losses, with dashed connectors between steps.
/// With `show_total` a final bar for the cumulative result is appended under
/// `total_label`. `title`, `width`, `height` and `theme` are optional.
pub fn waterfall_chart(
  data : Array[(String, Double)],
  show_total? : Bool = true,
  total_label? : String = "Total",
  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 = 48.0
  let plot_w = width - left - right
  let plot_h = height - top - bottom
  // Walk the running total once to find the value domain (zero included).
  let mut running = 0.0
  let mut lo = 0.0
  let mut hi = 0.0
  for d in data {
    running += d.1
    if running < lo {
      lo = running
    }
    if running > hi {
      hi = running
    }
  }
  let total = running
  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 slots = if show_total { n + 1 } else { n }
  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 / slots.to_double()
    let bar_w = slot * 0.6
    let sy = fn(v : Double) -> Double {
      scale_linear(v, axis_lo, axis_hi, top + plot_h, top)
    }
    let mut acc = 0.0
    for i in 0..= 0.0 { up_color } else { down_color }
      let top_v = if start > end { start } else { end }
      let bot_v = if start > end { end } else { start }
      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(x)),
          ("y", num(sy(top_v))),
          ("width", num(bar_w)),
          ("height", num(h)),
          ("fill", color),
          ("rx", "1"),
        ]),
      )
      // Dashed connector from this bar's end to the next bar's start.
      if i < n - 1 || show_total {
        body.write_string(
          elem("line", [
            ("x1", num(x + bar_w)),
            ("y1", num(sy(end))),
            ("x2", num(x + slot)),
            ("y2", num(sy(end))),
            ("stroke", theme.axis),
            ("stroke-width", "1"),
            ("stroke-dasharray", "3 3"),
          ]),
        )
      }
      body.write_string(
        label(x + bar_w / 2.0, top + plot_h + 16.0, name, fill=theme.text),
      )
      acc = end
    }
    // The cumulative total as a final anchored bar.
    if show_total {
      let x = left + slot * n.to_double() + (slot - bar_w) / 2.0
      let top_v = if total > 0.0 { total } else { 0.0 }
      let bot_v = if total > 0.0 { 0.0 } else { total }
      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(x)),
          ("y", num(sy(top_v))),
          ("width", num(bar_w)),
          ("height", num(h)),
          ("fill", theme.color_at(0)),
          ("rx", "1"),
        ]),
      )
      body.write_string(
        label(
          x + bar_w / 2.0,
          top + plot_h + 16.0,
          total_label,
          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())
}