///|
fn stream_finite(value : Double) -> Bool {
  !value.is_nan() && !value.is_inf()
}

///|
fn validate_stream_axis(
  values : Array[Double],
  name : String,
) -> Unit raise @core.DataError {
  if values.length() < 2 {
    raise @core.DataError::InvalidStreamplot(
      message="streamplot \{name} must have at least two coordinates",
    )
  }
  for value in values {
    if !stream_finite(value) {
      raise @core.DataError::InvalidStreamplot(
        message="streamplot coordinates must be finite",
      )
    }
  }
  let span = values[values.length() - 1] - values[0]
  let spacing = span / Double::from_int(values.length() - 1)
  if !stream_finite(span) {
    raise @core.DataError::InvalidStreamplot(
      message="streamplot \{name} coordinate span must be finite",
    )
  }
  for index in 1..
      1.0e-8 + 1.0e-5 * Double::abs(spacing) {
      raise @core.DataError::InvalidStreamplot(
        message="streamplot \{name} coordinates must be equally spaced",
      )
    }
  }
}

///|
fn validate_stream_field(
  field : Array[Array[Double]],
  rows : Int32,
  columns : Int32,
  name : String,
) -> Unit raise @core.DataError {
  if field.length() != rows {
    raise @core.DataError::InvalidStreamplot(
      message="streamplot \{name} rows must match y coordinates",
    )
  }
  for row in field {
    if row.length() != columns {
      raise @core.DataError::InvalidStreamplot(
        message="streamplot \{name} columns must match x coordinates",
      )
    }
  }
}

///|
fn stream_locate(values : Array[Double], value : Double) -> Int32? {
  for index in 0..<(values.length() - 1) {
    let first = values[index]
    let second = values[index + 1]
    if value >= first && value <= second {
      return Some(index)
    }
  }
  None
}

///|
fn stream_sample(
  xs : Array[Double],
  ys : Array[Double],
  us : Array[Array[Double]],
  vs : Array[Array[Double]],
  x : Double,
  y : Double,
) -> (Double, Double)? {
  let column = match stream_locate(xs, x) {
    Some(value) => value
    None => return None
  }
  let row = match stream_locate(ys, y) {
    Some(value) => value
    None => return None
  }
  let x0 = xs[column]
  let x1 = xs[column + 1]
  let y0 = ys[row]
  let y1 = ys[row + 1]
  let tx = (x - x0) / (x1 - x0)
  let ty = (y - y0) / (y1 - y0)
  let u00 = us[row][column]
  let u01 = us[row][column + 1]
  let u10 = us[row + 1][column]
  let u11 = us[row + 1][column + 1]
  let v00 = vs[row][column]
  let v01 = vs[row][column + 1]
  let v10 = vs[row + 1][column]
  let v11 = vs[row + 1][column + 1]
  if !stream_finite(u00) ||
    !stream_finite(u01) ||
    !stream_finite(u10) ||
    !stream_finite(u11) ||
    !stream_finite(v00) ||
    !stream_finite(v01) ||
    !stream_finite(v10) ||
    !stream_finite(v11) {
    return None
  }
  let u0 = u00 + (u01 - u00) * tx
  let u1 = u10 + (u11 - u10) * tx
  let v0 = v00 + (v01 - v00) * tx
  let v1 = v10 + (v11 - v10) * tx
  Some((u0 + (u1 - u0) * ty, v0 + (v1 - v0) * ty))
}

///|
fn stream_unit_direction(
  xs : Array[Double],
  ys : Array[Double],
  us : Array[Array[Double]],
  vs : Array[Array[Double]],
  x : Double,
  y : Double,
  x_span : Double,
  y_span : Double,
) -> (Double, Double)? {
  match stream_sample(xs, ys, us, vs, x, y) {
    None => None
    Some((u, v)) => {
      let speed = (u / x_span * (u / x_span) + v / y_span * (v / y_span)).sqrt()
      if speed <= 1.0e-12 || !stream_finite(speed) {
        None
      } else {
        // Return data-space displacement per unit axes-space arc length.
        Some((u / speed, v / speed))
      }
    }
  }
}

///|
/// Bilinearly interpolate one scalar grid at a data coordinate.
fn stream_sample_scalar(
  xs : Array[Double],
  ys : Array[Double],
  values : Array[Array[Double]],
  x : Double,
  y : Double,
) -> Double? {
  let column = match stream_locate(xs, x) {
    Some(value) => value
    None => return None
  }
  let row = match stream_locate(ys, y) {
    Some(value) => value
    None => return None
  }
  let x0 = xs[column]
  let x1 = xs[column + 1]
  let y0 = ys[row]
  let y1 = ys[row + 1]
  let tx = (x - x0) / (x1 - x0)
  let ty = (y - y0) / (y1 - y0)
  let v00 = values[row][column]
  let v01 = values[row][column + 1]
  let v10 = values[row + 1][column]
  let v11 = values[row + 1][column + 1]
  if !stream_finite(v00) ||
    !stream_finite(v01) ||
    !stream_finite(v10) ||
    !stream_finite(v11) {
    return None
  }
  let v0 = v00 + (v01 - v00) * tx
  let v1 = v10 + (v11 - v10) * tx
  Some(v0 + (v1 - v0) * ty)
}

///|
fn stream_validate_style_grid(
  values : Array[Array[Double]],
  rows : Int32,
  columns : Int32,
  name : String,
  allow_missing : Bool,
) -> Unit raise @core.DataError {
  validate_stream_field(values, rows, columns, name)
  for row in values {
    for value in row {
      if !allow_missing && !stream_finite(value) {
        raise @core.DataError::InvalidStreamplot(
          message="streamplot \{name} values must be finite",
        )
      }
    }
  }
}

///|
fn stream_grid_values(values : Array[Array[Double]]) -> Array[Double] {
  let result : Array[Double] = []
  for row in values {
    for value in row {
      if stream_finite(value) {
        result.push(value)
      }
    }
  }
  result
}

///|
fn stream_color_from_value(
  value : Double,
  norm : @colors.ColorNorm,
  cmap : @colors.ColorMap,
) -> String? {
  let normalized = norm.transform(value) catch { _ => return None }
  Some(cmap.sample_extended(normalized).to_css())
}

///|
fn stream_inside(
  xs : Array[Double],
  ys : Array[Double],
  x : Double,
  y : Double,
) -> Bool {
  x >= Double::min(xs[0], xs[xs.length() - 1]) &&
  x <= Double::max(xs[0], xs[xs.length() - 1]) &&
  y >= Double::min(ys[0], ys[ys.length() - 1]) &&
  y <= Double::max(ys[0], ys[ys.length() - 1])
}

///|
/// Take an Euler step from a point to the first data-domain boundary.
///
/// The adaptive corrector can sample just outside the domain near an edge.
/// Matplotlib adds one final Euler point in that case so streamlines end on
/// the boundary instead of stopping one integration step early.
fn stream_boundary_step(
  xs : Array[Double],
  ys : Array[Double],
  x : Double,
  y : Double,
  dx : Double,
  dy : Double,
  max_distance : Double,
) -> (@core.Point, Double)? {
  let xmin = xs[0]
  let xmax = xs[xs.length() - 1]
  let ymin = ys[0]
  let ymax = ys[ys.length() - 1]
  let x_distance = if dx > 0.0 {
    Some((xmax - x) / dx)
  } else if dx < 0.0 {
    Some((xmin - x) / dx)
  } else {
    None
  }
  let y_distance = if dy > 0.0 {
    Some((ymax - y) / dy)
  } else if dy < 0.0 {
    Some((ymin - y) / dy)
  } else {
    None
  }
  let distance = match (x_distance, y_distance) {
    (Some(x_value), Some(y_value)) => Double::min(x_value, y_value)
    (Some(value), None) => value
    (None, Some(value)) => value
    (None, None) => return None
  }
  if distance <= 0.0 || !stream_finite(distance) {
    return None
  }
  let actual_distance = Double::min(distance, max_distance)
  Some(
    (
      @core.point(x + dx * actual_distance, y + dy * actual_distance),
      actual_distance,
    ),
  )
}

///|
fn integrate_stream(
  xs : Array[Double],
  ys : Array[Double],
  us : Array[Array[Double]],
  vs : Array[Array[Double]],
  start_x : Double,
  start_y : Double,
  direction : Double,
  max_steps : Int32,
  x_span : Double,
  y_span : Double,
  max_length : Double,
  mask_width : Int32,
  mask_height : Int32,
  integration_max_step_scale : Double,
  integration_max_error_scale : Double,
  occupancy : Array[Bool],
  touched : Array[Int32],
  broken_streamlines : Bool,
  xmin : Double,
  xmax : Double,
  ymin : Double,
  ymax : Double,
) -> Array[@core.Point] {
  let result = Array(capacity=max_steps + 1)
  let mut x = start_x
  let mut y = start_y
  let mut travelled = 0.0
  let max_step = Double::min(
      Double::min(
        1.0 / Double::from_int(mask_width),
        1.0 / Double::from_int(mask_height),
      ),
      0.1,
    ) *
    integration_max_step_scale
  let max_error = 0.003 * integration_max_error_scale
  result.push(@core.point(x, y))
  let mut previous_cell = Some(
    stream_cell(start_x, xmin, xmax, mask_width) +
    stream_cell(start_y, ymin, ymax, mask_height) * mask_width,
  )
  let mut step = max_step
  for _ in 0.. attempts = 10
        Some((k1_x, k1_y)) => {
          let euler_x = x + direction * step * k1_x
          let euler_y = y + direction * step * k1_y
          match
            stream_unit_direction(
              xs, ys, us, vs, euler_x, euler_y, x_span, y_span,
            ) {
            None =>
              if !stream_inside(xs, ys, euler_x, euler_y) {
                match
                  stream_boundary_step(
                    xs,
                    ys,
                    x,
                    y,
                    direction * k1_x,
                    direction * k1_y,
                    remaining,
                  ) {
                  None => attempts = 10
                  Some((boundary_point, boundary_distance)) => {
                    let (can_continue, next_cell) = stream_update_mask(
                      boundary_point, previous_cell, occupancy, touched, mask_width,
                      mask_height, xmin, xmax, ymin, ymax, broken_streamlines,
                    )
                    if !can_continue {
                      attempts = 10
                    } else {
                      x = boundary_point.x()
                      y = boundary_point.y()
                      previous_cell = Some(next_cell)
                      travelled += boundary_distance
                      result.push(boundary_point)
                      accepted = true
                    }
                  }
                }
              } else {
                attempts = 10
              }
            Some((k2_x, k2_y)) => {
              let corrected_x = direction * step * (k1_x + k2_x) / 2.0
              let corrected_y = direction * step * (k1_y + k2_y) / 2.0
              let error = ((corrected_x - direction * step * k1_x) /
              x_span *
              ((corrected_x - direction * step * k1_x) / x_span) +
              (corrected_y - direction * step * k1_y) /
              y_span *
              ((corrected_y - direction * step * k1_y) / y_span)).sqrt()
              if error > max_error && step > 1.0e-12 {
                step = step * 0.5
                attempts += 1
              } else {
                let proposed_length = (corrected_x /
                x_span *
                (corrected_x / x_span) +
                corrected_y / y_span * (corrected_y / y_span)).sqrt()
                let factor = if proposed_length > remaining {
                  remaining / proposed_length
                } else {
                  1.0
                }
                let next_x = x + corrected_x * factor
                let next_y = y + corrected_y * factor
                if !stream_inside(xs, ys, next_x, next_y) {
                  attempts = 10
                } else {
                  let next_point = @core.point(next_x, next_y)
                  let (can_continue, next_cell) = stream_update_mask(
                    next_point, previous_cell, occupancy, touched, mask_width, mask_height,
                    xmin, xmax, ymin, ymax, broken_streamlines,
                  )
                  if !can_continue {
                    attempts = 10
                  } else {
                    x = next_x
                    y = next_y
                    previous_cell = Some(next_cell)
                    travelled += proposed_length * factor
                    result.push(next_point)
                    if error <= 1.0e-12 {
                      step = max_step
                    } else {
                      step = Double::min(
                        max_step,
                        0.85 * step * (max_error / error).sqrt(),
                      )
                    }
                    accepted = true
                    if factor < 1.0 {
                      attempts = 10
                    }
                  }
                }
              }
            }
          }
        }
      }
    }
    if !accepted {
      break
    }
  }
  result
}

///|
fn stream_length(
  line : Array[@core.Point],
  x_span : Double,
  y_span : Double,
) -> Double {
  let mut result = 0.0
  for index in 1.. Int32 {
  if cells <= 1 {
    return 0
  }
  // Matplotlib's DomainMap rounds grid coordinates to the nearest mask
  // coordinate.  The mask coordinate range is 0..(cells - 1), so the
  // right/top boundary maps directly to the final cell.
  let raw = Double::round(
    (value - lower) / (upper - lower) * Double::from_int(cells - 1),
  ).to_int()
  Int32::max(0, Int32::min(cells - 1, raw))
}

///|
fn stream_mask_coordinate(
  index : Int32,
  cells : Int32,
  lower : Double,
  upper : Double,
) -> Double {
  if cells <= 1 {
    (lower + upper) / 2.0
  } else {
    lower +
    (upper - lower) * Double::from_int(index) / Double::from_int(cells - 1)
  }
}

///|
/// Mark one mask cell for the current trajectory.
///
/// The returned boolean says whether integration may continue.  A trajectory
/// may revisit the cell it occupied in its immediately preceding step; an
/// occupied cell belonging to an earlier part of the field terminates a
/// broken streamline.  When broken streamlines are disabled, existing cells
/// are traversable but are not added to the transaction being built.
fn stream_update_mask(
  point : @core.Point,
  previous_cell : Int32?,
  occupancy : Array[Bool],
  touched : Array[Int32],
  mask_width : Int32,
  mask_height : Int32,
  xmin : Double,
  xmax : Double,
  ymin : Double,
  ymax : Double,
  broken_streamlines : Bool,
) -> (Bool, Int32) {
  let column = stream_cell(point.x(), xmin, xmax, mask_width)
  let row = stream_cell(point.y(), ymin, ymax, mask_height)
  let cell = row * mask_width + column
  if previous_cell is Some(previous) && previous == cell {
    return (true, cell)
  }
  if occupancy[cell] {
    if broken_streamlines {
      return (false, cell)
    }
    return (true, cell)
  }
  occupancy[cell] = true
  touched.push(cell)
  (true, cell)
}

///|
fn undo_stream_mask(occupancy : Array[Bool], touched : Array[Int32]) -> Unit {
  for cell in touched {
    occupancy[cell] = false
  }
  touched.clear()
}

///|
/// Generate mask seeds in the boundary-first spiral used by Matplotlib.
fn stream_seed_points(
  mask_width : Int32,
  mask_height : Int32,
  xmin : Double,
  xmax : Double,
  ymin : Double,
  ymax : Double,
) -> Array[@core.Point] {
  let result : Array[@core.Point] = Array(capacity=mask_width * mask_height)
  if mask_width <= 1 {
    for row in 0..= xlast {
        xlast -= 1
        direction = 1
      }
    } else if direction == 1 {
      y += 1
      if y >= ylast {
        ylast -= 1
        direction = 2
      }
    } else if direction == 2 {
      x -= 1
      if x <= xfirst {
        xfirst += 1
        direction = 3
      }
    } else {
      y -= 1
      if y <= yfirst {
        yfirst += 1
        direction = 0
      }
    }
  }
  result
}

///|
fn stream_join(
  backward : Array[@core.Point],
  forward : Array[@core.Point],
) -> Array[@core.Point] {
  let result = Array(capacity=backward.length() + forward.length())
  if backward.length() > 0 {
    for index in 0.. 1 {
    for index in 1.. (String, Double)? {
  let width = match linewidth_grid {
    None => Some(linewidth)
    Some(values) => stream_sample_scalar(xs, ys, values, point.x(), point.y())
  }
  let line_color = match color_grid {
    None => Some(color)
    Some(values) => {
      let value = match
        stream_sample_scalar(xs, ys, values, point.x(), point.y()) {
        Some(value) => value
        None => return None
      }
      match (color_norm, color_map) {
        (Some(norm), Some(cmap)) => stream_color_from_value(value, norm, cmap)
        _ => None
      }
    }
  }
  match (line_color, width) {
    (Some(line_color), Some(width)) => Some((line_color, width))
    _ => None
  }
}

///|
fn stream_line_artists(
  axes : Axes,
  xs : Array[Double],
  ys : Array[Double],
  line : Array[@core.Point],
  linewidth : Double,
  color : String,
  linewidth_grid : Array[Array[Double]]?,
  color_grid : Array[Array[Double]]?,
  color_norm : @colors.ColorNorm?,
  color_map : @colors.ColorMap?,
) -> Array[Artist] {
  let result : Array[Artist] = []
  if linewidth_grid is None && color_grid is None {
    let artist = Artist::Line(line, @core.style(color, width=linewidth))
    axes.push_artist(artist)
    result.push(artist)
    return result
  }
  for index in 1.. {
        let artist = Artist::Line(
          [first, second],
          @core.style(line_color, width=line_width),
        )
        axes.push_artist(artist)
        result.push(artist)
      }
      None => ()
    }
  }
  result
}

///|
fn streamplot_arrowstyle_valid(value : String) -> Bool {
  value == "-|>" ||
  value == "->" ||
  value == "-" ||
  value == "<-" ||
  value == "<->" ||
  value == "<|-" ||
  value == "<|-|>" ||
  value == "]-" ||
  value == "-[" ||
  value == "]-[" ||
  value == "|-|" ||
  value == "fancy" ||
  value == "simple" ||
  value == "wedge"
}

///|
fn streamplot_error(message~ : String) -> @core.DataError {
  @core.DataError::InvalidStreamplot(message~)
}

///|
fn resolve_stream_color_map(
  name : String,
) -> @colors.ColorMap raise @core.DataError {
  @colors.colormap(name) catch {
    _ => raise streamplot_error(message="unknown streamplot colormap")
  }
}

///|
fn streamplot_norm(
  values : Array[Array[Double]],
  vmin : Double?,
  vmax : Double?,
) -> @colors.ColorNorm raise @core.DataError {
  let finite_values = stream_grid_values(values)
  if finite_values.length() == 0 {
    raise streamplot_error(
      message="streamplot color values must contain finite data",
    )
  }
  let mut lower = match vmin {
    Some(value) => value
    None => finite_values[0]
  }
  let mut upper = match vmax {
    Some(value) => value
    None => finite_values[0]
  }
  if vmin is None || vmax is None {
    for value in finite_values {
      if vmin is None {
        lower = Double::min(lower, value)
      }
      if vmax is None {
        upper = Double::max(upper, value)
      }
    }
  }
  if !stream_finite(lower) || !stream_finite(upper) || lower > upper {
    raise streamplot_error(
      message="streamplot color bounds must be finite and ordered",
    )
  }
  @colors.ColorNorm::Linear(
    @colors.normalize(vmin=lower, vmax=upper) catch {
      _ => raise streamplot_error(message="invalid streamplot color bounds")
    },
  )
}

///|
fn add_streamlines(
  axes : Axes,
  xs : Array[Double],
  ys : Array[Double],
  us : Array[Array[Double]],
  vs : Array[Array[Double]],
  density_x : Double,
  density_y : Double,
  linewidth : Double,
  max_steps : Int32,
  minlength : Double,
  maxlength : Double,
  start_points : Array[@core.Point],
  integration_direction : String,
  color : String,
  arrows : Bool,
  arrow_size : Double,
  arrowstyle : String,
  num_arrows : Int32,
  broken_streamlines : Bool,
  integration_max_step_scale : Double,
  integration_max_error_scale : Double,
  linewidth_grid : Array[Array[Double]]?,
  color_grid : Array[Array[Double]]?,
  color_norm : @colors.ColorNorm?,
  color_map : @colors.ColorMap?,
) -> StreamplotSet raise @core.DataError {
  let x_span = xs[xs.length() - 1] - xs[0]
  let y_span = ys[ys.length() - 1] - ys[0]
  let mask_width = Int32::max(1, (30.0 * density_x).to_int())
  let mask_height = Int32::max(1, (30.0 * density_y).to_int())
  let occupancy : Array[Bool] = Array(capacity=mask_width * mask_height)
  for _ in 0..<(mask_width * mask_height) {
    occupancy.push(false)
  }
  let lines : Array[Artist] = []
  let arrows_result : Array[Artist] = []
  let seeds = if start_points.length() > 0 {
    start_points.copy()
  } else {
    stream_seed_points(
      mask_width,
      mask_height,
      xs[0],
      xs[xs.length() - 1],
      ys[0],
      ys[ys.length() - 1],
    )
  }
  let directional_maxlength = if integration_direction == "both" {
    maxlength / 2.0
  } else {
    maxlength
  }
  for seed in seeds {
    let touched : Array[Int32] = []
    let (seed_can_continue, _) = stream_update_mask(
      seed,
      None,
      occupancy,
      touched,
      mask_width,
      mask_height,
      xs[0],
      xs[xs.length() - 1],
      ys[0],
      ys[ys.length() - 1],
      broken_streamlines,
    )
    if !seed_can_continue {
      continue
    }
    let backward = if integration_direction == "forward" {
      [seed]
    } else {
      integrate_stream(
        xs,
        ys,
        us,
        vs,
        seed.x(),
        seed.y(),
        -1.0,
        max_steps,
        x_span,
        y_span,
        directional_maxlength,
        mask_width,
        mask_height,
        integration_max_step_scale,
        integration_max_error_scale,
        occupancy,
        touched,
        broken_streamlines,
        xs[0],
        xs[xs.length() - 1],
        ys[0],
        ys[ys.length() - 1],
      )
    }
    let forward = if integration_direction == "backward" {
      [seed]
    } else {
      integrate_stream(
        xs,
        ys,
        us,
        vs,
        seed.x(),
        seed.y(),
        1.0,
        max_steps,
        x_span,
        y_span,
        directional_maxlength,
        mask_width,
        mask_height,
        integration_max_step_scale,
        integration_max_error_scale,
        occupancy,
        touched,
        broken_streamlines,
        xs[0],
        xs[xs.length() - 1],
        ys[0],
        ys[ys.length() - 1],
      )
    }
    let line = stream_join(backward, forward)
    if line.length() >= 2 && stream_length(line, x_span, y_span) >= minlength {
      let line_artists = stream_line_artists(
        axes, xs, ys, line, linewidth, color, linewidth_grid, color_grid, color_norm,
        color_map,
      )
      for artist in line_artists {
        lines.push(artist)
      }
      if arrows {
        let arrow_artists = add_stream_arrows(
          axes, line, num_arrows, color, linewidth, arrow_size, arrowstyle, x_span,
          y_span, xs, ys, linewidth_grid, color_grid, color_norm, color_map,
        )
        for artist in arrow_artists {
          arrows_result.push(artist)
        }
      }
    } else {
      // A short trajectory must not reserve mask cells for later seeds.
      undo_stream_mask(occupancy, touched)
    }
  }
  streamplot_set(lines, arrows_result)
}

///|
fn validate_streamplot_inputs(
  xs : Array[Double],
  ys : Array[Double],
  us : Array[Array[Double]],
  vs : Array[Array[Double]],
  density : Double,
  density_x : Double,
  density_y : Double,
  linewidth : Double,
  max_steps : Int32,
  minlength : Double,
  maxlength : Double,
  start_points : Array[@core.Point],
  integration_direction : String,
  num_arrows : Int32,
  integration_max_step_scale : Double,
  integration_max_error_scale : Double,
  arrow_size : Double,
  arrowstyle : String,
  linewidth_grid : Array[Array[Double]]?,
  color_grid : Array[Array[Double]]?,
) -> Unit raise @core.DataError {
  validate_stream_axis(xs, "x")
  validate_stream_axis(ys, "y")
  validate_stream_field(us, ys.length(), xs.length(), "u")
  validate_stream_field(vs, ys.length(), xs.length(), "v")
  if !stream_finite(density) || density <= 0.0 {
    raise streamplot_error(
      message="streamplot density must be finite and positive",
    )
  }
  if (
      !stream_finite(density_x) ||
      density_x == 0.0 ||
      (density_x < 0.0 && density_x != -1.0)
    ) ||
    !stream_finite(density_y) ||
    density_y == 0.0 ||
    (density_y < 0.0 && density_y != -1.0) {
    raise streamplot_error(
      message="streamplot density_x and density_y must be positive",
    )
  }
  if !stream_finite(linewidth) || linewidth < 0.0 {
    raise streamplot_error(
      message="streamplot linewidth must be finite and non-negative",
    )
  }
  if max_steps < 2 {
    raise streamplot_error(message="streamplot max_steps must be at least two")
  }
  if !stream_finite(minlength) || minlength < 0.0 {
    raise streamplot_error(
      message="streamplot minlength must be finite and non-negative",
    )
  }
  if !stream_finite(maxlength) || maxlength <= 0.0 {
    raise streamplot_error(
      message="streamplot maxlength must be finite and positive",
    )
  }
  if integration_direction != "both" &&
    integration_direction != "forward" &&
    integration_direction != "backward" {
    raise streamplot_error(
      message="streamplot integration_direction must be both, forward, or backward",
    )
  }
  for point in start_points {
    if !point.is_finite() || !stream_inside(xs, ys, point.x(), point.y()) {
      raise streamplot_error(
        message="streamplot start_points must be finite and inside the grid",
      )
    }
  }
  if num_arrows < 0 {
    raise streamplot_error(message="streamplot num_arrows must be non-negative")
  }
  if !stream_finite(integration_max_step_scale) ||
    integration_max_step_scale <= 0.0 {
    raise streamplot_error(
      message="streamplot integration_max_step_scale must be positive",
    )
  }
  if !stream_finite(integration_max_error_scale) ||
    integration_max_error_scale <= 0.0 {
    raise streamplot_error(
      message="streamplot integration_max_error_scale must be positive",
    )
  }
  if !stream_finite(arrow_size) || arrow_size < 0.0 {
    raise streamplot_error(
      message="streamplot arrow_size must be finite and non-negative",
    )
  }
  if !streamplot_arrowstyle_valid(arrowstyle) {
    raise streamplot_error(
      message="streamplot arrowstyle is not a supported FancyArrowPatch style",
    )
  }
  match linewidth_grid {
    Some(values) =>
      stream_validate_style_grid(
        values,
        ys.length(),
        xs.length(),
        "linewidth",
        false,
      )
    None => ()
  }
  match color_grid {
    Some(values) =>
      stream_validate_style_grid(
        values,
        ys.length(),
        xs.length(),
        "color",
        true,
      )
    None => ()
  }
}

///|
fn Axes::streamplot_impl(
  self : Axes,
  xs : Array[Double],
  ys : Array[Double],
  us : Array[Array[Double]],
  vs : Array[Array[Double]],
  density : Double,
  density_x : Double,
  density_y : Double,
  color : String,
  linewidth : Double,
  max_steps : Int32,
  minlength : Double,
  start_points : Array[@core.Point],
  maxlength : Double,
  integration_direction : String,
  broken_streamlines : Bool,
  integration_max_step_scale : Double,
  integration_max_error_scale : Double,
  arrows : Bool,
  arrow_size : Double,
  arrowstyle : String,
  num_arrows : Int32,
  label : String,
  linewidth_grid : Array[Array[Double]]?,
  color_grid : Array[Array[Double]]?,
  cmap : String?,
  vmin : Double?,
  vmax : Double?,
) -> StreamplotSet raise @core.DataError {
  validate_streamplot_inputs(
    xs, ys, us, vs, density, density_x, density_y, linewidth, max_steps, minlength,
    maxlength, start_points, integration_direction, num_arrows, integration_max_step_scale,
    integration_max_error_scale, arrow_size, arrowstyle, linewidth_grid, color_grid,
  )
  let (color_norm, color_map) = match color_grid {
    None => (None, None)
    Some(values) => {
      let map_name = match cmap {
        Some(value) => value
        None => "viridis"
      }
      (
        Some(streamplot_norm(values, vmin, vmax)),
        Some(resolve_stream_color_map(map_name)),
      )
    }
  }
  let resolved_color = self.resolve_color(color)
  let start = self.state.val.artists.length()
  let result = add_streamlines(
    self,
    xs,
    ys,
    us,
    vs,
    if density_x < 0.0 {
      density
    } else {
      density_x
    },
    if density_y < 0.0 {
      density
    } else {
      density_y
    },
    linewidth,
    max_steps,
    minlength,
    maxlength,
    start_points,
    integration_direction,
    resolved_color,
    arrows,
    arrow_size,
    arrowstyle,
    num_arrows,
    broken_streamlines,
    integration_max_step_scale,
    integration_max_error_scale,
    linewidth_grid,
    color_grid,
    color_norm,
    color_map,
  )
  self.add_auto_legend(label, resolved_color, LegendKind::Line)
  self.add_container(ContainerKind::Streamplot, start, label)
  result
}

///|
/// Draw streamlines for a regular vector field on 1D x/y grids.
pub fn Figure::streamplot(
  self : Figure,
  xs : Array[Double],
  ys : Array[Double],
  us : Array[Array[Double]],
  vs : Array[Array[Double]],
  density~ : Double = 1.0,
  density_x~ : Double = -1.0,
  density_y~ : Double = -1.0,
  color~ : String = "",
  linewidth~ : Double = 1.0,
  max_steps~ : Int32 = 200,
  minlength~ : Double = 0.1,
  start_points~ : Array[@core.Point] = [],
  maxlength~ : Double = 4.0,
  integration_direction~ : String = "both",
  broken_streamlines~ : Bool = true,
  integration_max_step_scale~ : Double = 1.0,
  integration_max_error_scale~ : Double = 1.0,
  arrows~ : Bool = true,
  arrow_size~ : Double = 1.0,
  num_arrows~ : Int32 = 1,
  arrowstyle~ : String = "-|>",
  label~ : String = "",
) -> StreamplotSet raise @core.DataError {
  self
  .axes()
  .streamplot(
    xs,
    ys,
    us,
    vs,
    density~,
    density_x~,
    density_y~,
    color~,
    linewidth~,
    max_steps~,
    minlength~,
    start_points~,
    maxlength~,
    integration_direction~,
    broken_streamlines~,
    integration_max_step_scale~,
    integration_max_error_scale~,
    arrows~,
    arrow_size~,
    num_arrows~,
    arrowstyle~,
    label~,
  )
}

///|
/// Draw streamlines and optionally vary line width and color over the grid.
///
/// Empty style grids use the scalar `color` and `linewidth` values. Color
/// values are mapped through `cmap` after linear normalization.
pub fn Figure::streamplot_with_style_grids(
  self : Figure,
  xs : Array[Double],
  ys : Array[Double],
  us : Array[Array[Double]],
  vs : Array[Array[Double]],
  linewidths~ : Array[Array[Double]] = [],
  color_values~ : Array[Array[Double]] = [],
  density~ : Double = 1.0,
  density_x~ : Double = -1.0,
  density_y~ : Double = -1.0,
  color~ : String = "",
  linewidth~ : Double = 1.0,
  cmap~ : String = "viridis",
  vmin? : Double,
  vmax? : Double,
  max_steps~ : Int32 = 200,
  minlength~ : Double = 0.1,
  start_points~ : Array[@core.Point] = [],
  maxlength~ : Double = 4.0,
  integration_direction~ : String = "both",
  broken_streamlines~ : Bool = true,
  integration_max_step_scale~ : Double = 1.0,
  integration_max_error_scale~ : Double = 1.0,
  arrows~ : Bool = true,
  arrow_size~ : Double = 1.0,
  num_arrows~ : Int32 = 1,
  arrowstyle~ : String = "-|>",
  label~ : String = "",
) -> StreamplotSet raise @core.DataError {
  let linewidth_grid = if linewidths.length() == 0 {
    None
  } else {
    Some(linewidths)
  }
  let color_grid = if color_values.length() == 0 {
    None
  } else {
    Some(color_values)
  }
  self
  .axes()
  .streamplot_impl(
    xs,
    ys,
    us,
    vs,
    density,
    density_x,
    density_y,
    color,
    linewidth,
    max_steps,
    minlength,
    start_points,
    maxlength,
    integration_direction,
    broken_streamlines,
    integration_max_step_scale,
    integration_max_error_scale,
    arrows,
    arrow_size,
    arrowstyle,
    num_arrows,
    label,
    linewidth_grid,
    color_grid,
    Some(cmap),
    vmin,
    vmax,
  )
}

///|
/// Draw streamlines and optionally vary line width and color over the grid.
pub fn Axes::streamplot(
  self : Axes,
  xs : Array[Double],
  ys : Array[Double],
  us : Array[Array[Double]],
  vs : Array[Array[Double]],
  density~ : Double = 1.0,
  density_x~ : Double = -1.0,
  density_y~ : Double = -1.0,
  color~ : String = "",
  linewidth~ : Double = 1.0,
  max_steps~ : Int32 = 200,
  minlength~ : Double = 0.1,
  start_points~ : Array[@core.Point] = [],
  maxlength~ : Double = 4.0,
  integration_direction~ : String = "both",
  broken_streamlines~ : Bool = true,
  integration_max_step_scale~ : Double = 1.0,
  integration_max_error_scale~ : Double = 1.0,
  arrows~ : Bool = true,
  arrow_size~ : Double = 1.0,
  num_arrows~ : Int32 = 1,
  arrowstyle~ : String = "-|>",
  label~ : String = "",
) -> StreamplotSet raise @core.DataError {
  self.streamplot_impl(
    xs,
    ys,
    us,
    vs,
    density,
    density_x,
    density_y,
    color,
    linewidth,
    max_steps,
    minlength,
    start_points,
    maxlength,
    integration_direction,
    broken_streamlines,
    integration_max_step_scale,
    integration_max_error_scale,
    arrows,
    arrow_size,
    arrowstyle,
    num_arrows,
    label,
    None,
    None,
    None,
    None,
    None,
  )
}

///|
/// Draw streamlines with optional grid-valued linewidth and color controls.
pub fn Axes::streamplot_with_style_grids(
  self : Axes,
  xs : Array[Double],
  ys : Array[Double],
  us : Array[Array[Double]],
  vs : Array[Array[Double]],
  linewidths~ : Array[Array[Double]] = [],
  color_values~ : Array[Array[Double]] = [],
  density~ : Double = 1.0,
  density_x~ : Double = -1.0,
  density_y~ : Double = -1.0,
  color~ : String = "",
  linewidth~ : Double = 1.0,
  cmap~ : String = "viridis",
  vmin? : Double,
  vmax? : Double,
  max_steps~ : Int32 = 200,
  minlength~ : Double = 0.1,
  start_points~ : Array[@core.Point] = [],
  maxlength~ : Double = 4.0,
  integration_direction~ : String = "both",
  broken_streamlines~ : Bool = true,
  integration_max_step_scale~ : Double = 1.0,
  integration_max_error_scale~ : Double = 1.0,
  arrows~ : Bool = true,
  arrow_size~ : Double = 1.0,
  num_arrows~ : Int32 = 1,
  arrowstyle~ : String = "-|>",
  label~ : String = "",
) -> StreamplotSet raise @core.DataError {
  let linewidth_grid = if linewidths.length() == 0 {
    None
  } else {
    Some(linewidths)
  }
  let color_grid = if color_values.length() == 0 {
    None
  } else {
    Some(color_values)
  }
  self.streamplot_impl(
    xs,
    ys,
    us,
    vs,
    density,
    density_x,
    density_y,
    color,
    linewidth,
    max_steps,
    minlength,
    start_points,
    maxlength,
    integration_direction,
    broken_streamlines,
    integration_max_step_scale,
    integration_max_error_scale,
    arrows,
    arrow_size,
    arrowstyle,
    num_arrows,
    label,
    linewidth_grid,
    color_grid,
    Some(cmap),
    vmin,
    vmax,
  )
}