// Box-and-whisker plot built on the quartile helpers in stats.mbt.

///|
/// Render a box plot as a standalone SVG document string: one box per
/// `(label, samples)` pair. Boxes span the interquartile range with a median
/// line; whiskers extend to the furthest samples within 1.5×IQR of the box,
/// and samples beyond the whiskers are drawn as outlier dots. `title`,
/// `width`, `height` and `theme` are optional.
pub fn box_plot(
  data : Array[(String, 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 = 40.0
  let plot_w = width - left - right
  let plot_h = height - top - bottom
  // Global value range across every sample of every series.
  let all : Array[Double] = []
  for d in data {
    for v in d.1 {
      all.push(v)
    }
  }
  let (raw_lo, raw_hi) = extent(all)
  let (axis_lo, axis_hi, ticks) = nice_ticks(raw_lo, raw_hi, 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 bw = slot * 0.45
    for i in 0.. 0 {
        let color = theme.color_at(i)
        let (q1, med, q3) = quartiles(samples)
        let iqr = q3 - q1
        let fence_lo = q1 - 1.5 * iqr
        let fence_hi = q3 + 1.5 * iqr
        // Whiskers reach the furthest samples inside the fences.
        let mut w_lo = q1
        let mut w_hi = q3
        for v in samples {
          if v >= fence_lo && v < w_lo {
            w_lo = v
          }
          if v <= fence_hi && v > w_hi {
            w_hi = v
          }
        }
        let sy = fn(v : Double) -> Double {
          scale_linear(v, axis_lo, axis_hi, top + plot_h, top)
        }
        // Whisker stem and end caps.
        body.write_string(
          elem("line", [
            ("x1", num(cx)),
            ("y1", num(sy(w_lo))),
            ("x2", num(cx)),
            ("y2", num(sy(w_hi))),
            ("stroke", color),
            ("stroke-width", "1.5"),
          ]),
        )
        for cap in [w_lo, w_hi] {
          body.write_string(
            elem("line", [
              ("x1", num(cx - bw * 0.3)),
              ("y1", num(sy(cap))),
              ("x2", num(cx + bw * 0.3)),
              ("y2", num(sy(cap))),
              ("stroke", color),
              ("stroke-width", "1.5"),
            ]),
          )
        }
        // The interquartile box with its median line.
        body.write_string(
          elem("rect", [
            ("x", num(cx - bw / 2.0)),
            ("y", num(sy(q3))),
            ("width", num(bw)),
            ("height", num(sy(q1) - sy(q3))),
            ("fill", color),
            ("fill-opacity", "0.25"),
            ("stroke", color),
            ("stroke-width", "1.5"),
            ("rx", "2"),
          ]),
        )
        body.write_string(
          elem("line", [
            ("x1", num(cx - bw / 2.0)),
            ("y1", num(sy(med))),
            ("x2", num(cx + bw / 2.0)),
            ("y2", num(sy(med))),
            ("stroke", color),
            ("stroke-width", "2.5"),
          ]),
        )
        // Outliers beyond the fences.
        for v in samples {
          if v < fence_lo || v > fence_hi {
            body.write_string(
              elem("circle", [
                ("cx", num(cx)),
                ("cy", num(sy(v))),
                ("r", "3"),
                ("fill", color),
                ("fill-opacity", "0.8"),
              ]),
            )
          }
        }
      }
    }
  }
  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())
}