buildMonotonePath method

Path buildMonotonePath(
  1. List<Offset> points, {
  2. bool close = false,
})

Builds a smooth path using monotone cubic interpolation (Fritsch–Carlson). Unlike a cardinal spline, this never overshoots the data: the curve introduces no false peaks, no dips below a local minimum, and stays within the value range between points — so a smoothed line remains a faithful reading of the data.

Works for x ordered strictly ascending or descending; because the interpolant is reversal-symmetric, feeding it a reversed point list yields the exact mirror curve — which keeps stacked-area band boundaries aligned. Falls back to the cardinal spline when x is not strictly monotonic.

Implementation

Path buildMonotonePath(List<Offset> points, {bool close = false}) {
  final n = points.length;
  if (n < 3) {
    final path = Path();
    if (n == 0) return path;
    path.moveTo(points.first.dx, points.first.dy);
    for (var i = 1; i < n; i++) {
      path.lineTo(points[i].dx, points[i].dy);
    }
    if (close) path.close();
    return path;
  }

  // x-deltas between consecutive points.
  final dx = List<double>.filled(n - 1, 0);
  for (var i = 0; i < n - 1; i++) {
    dx[i] = points[i + 1].dx - points[i].dx;
  }
  // Require strictly monotonic x (all deltas the same, non-zero sign);
  // otherwise x isn't a function of position — defer to the cardinal spline.
  final sign = dx[0].sign;
  if (sign == 0 || dx.any((d) => d.sign != sign)) {
    return buildSmoothPath(points, close: close);
  }

  // Secant slopes between consecutive points.
  final slope = List<double>.filled(n - 1, 0);
  for (var i = 0; i < n - 1; i++) {
    slope[i] = (points[i + 1].dy - points[i].dy) / dx[i];
  }

  // Tangents: average of neighbouring secants, clamped to 0 at extrema so
  // the curve can't overshoot a peak or trough.
  final m = List<double>.filled(n, 0);
  m[0] = slope[0];
  m[n - 1] = slope[n - 2];
  for (var i = 1; i < n - 1; i++) {
    m[i] = (slope[i - 1] * slope[i] <= 0)
        ? 0.0
        : (slope[i - 1] + slope[i]) / 2;
  }

  // Fritsch–Carlson: rein in tangents that would break monotonicity.
  for (var i = 0; i < n - 1; i++) {
    if (slope[i] == 0) {
      m[i] = 0;
      m[i + 1] = 0;
      continue;
    }
    final a = m[i] / slope[i];
    final b = m[i + 1] / slope[i];
    final h = a * a + b * b;
    if (h > 9) {
      final t = 3 / math.sqrt(h);
      m[i] = t * a * slope[i];
      m[i + 1] = t * b * slope[i];
    }
  }

  // Emit Hermite segments as cubic Béziers (control points at thirds).
  final path = Path()..moveTo(points.first.dx, points.first.dy);
  for (var i = 0; i < n - 1; i++) {
    final p1 = points[i];
    final p2 = points[i + 1];
    final third = dx[i] / 3;
    path.cubicTo(
      p1.dx + third,
      p1.dy + m[i] * third,
      p2.dx - third,
      p2.dy - m[i + 1] * third,
      p2.dx,
      p2.dy,
    );
  }
  if (close) path.close();
  return path;
}