background

The company recently introduced the Stack of Flutter technology, Google’s mobile UI framework for quickly building high-quality native user interfaces on iOS and Android. However, because Flutter is still in its early development stage, its ecological construction is not yet complete. For example, the chart UI component is needed in the project. After some research, Google/ Charts has the most powerful function and rich style (see online Gallery for details), so it was introduced into the project. However, Charts only implements straight line line charts, so the fork Charts project has to implement the smooth curve effect itself.

Based on using

  • The Ole/Charts library is powerful but not very document-friendly, with pure sample code in the online gallery and little Api explanation.

  • Demo effect of feasibility analysis

  • Carefully study the results of the optimization

  • Use code and comments
return Container(
  height: 150.0,
  child: charts.LineChart(
    _createChartData(), // The list of points on the line chart
    animate: true./ / animation
    defaultRenderer: charts.LineRendererConfig( // The configuration of line drawing
      includeArea: true,
      includePoints: true,
      includeLine: true,
      stacked: false,
    ),
    domainAxis: charts.NumericAxisSpec( // Spindle configuration
      tickFormatterSpec: DomainFormatterSpec(widget.dateRange), // Format the tick value, where num is converted to String
      renderSpec: charts.SmallTickRendererSpec( // The configuration of the spindle drawing
        tickLengthPx: 0.// The scale marks the length of the highlight
        labelOffsetFromAxisPx: 12.// Scale text distance from the axis of the displacement
        labelStyle: charts.TextStyleSpec( // The scale text style
          color: ChartUtil.getChartColor(HColors.lightGrey),
          fontSize: HFontSizes.smaller.toInt(),
        ),
        axisLineStyle: charts.LineStyleSpec( // Axis style
          color: ChartUtil.getChartColor(ChartUtil.lightBlue),
        ),
      ),
      tickProviderSpec: charts.BasicNumericTickProviderSpec( // Axis calibration configuration
        dataIsInWholeNumbers: false,
        desiredTickCount: widget.data.length, // Expect to display several ticks
      ),
    ),
    primaryMeasureAxis: charts.NumericAxisSpec( // Cross axis configuration, the parameters refer to the spindle configuration
      showAxisLine: false.// Display axis
      tickFormatterSpec: MeasureFormatterSpec(),
      tickProviderSpec: charts.BasicNumericTickProviderSpec(
        dataIsInWholeNumbers: false,
        desiredTickCount: 4,
      ),
      renderSpec: charts.GridlineRendererSpec( // Cross the axis calibration horizontal lines
        tickLengthPx: 0,
        labelOffsetFromAxisPx: 12,
        labelStyle: charts.TextStyleSpec(
          color: ChartUtil.getChartColor(HColors.lightGrey),
          fontSize: HFontSizes.smaller.toInt(),
        ),
        lineStyle: charts.LineStyleSpec(
          color: ChartUtil.getChartColor(ChartUtil.lightBlue),
        ),
        axisLineStyle: charts.LineStyleSpec(
          color: ChartUtil.getChartColor(ChartUtil.lightBlue),
        ),
      ),
    ),
    selectionModels: [ // Set the click selected event
      charts.SelectionModelConfig(
        type: charts.SelectionModelType.info,
        listener: _onSelectionChanged,
      )
    ],
    behaviors: [
      charts.InitialSelection(selectedDataConfig: [ // Set it to default
        charts.SeriesDatumConfig<num> ('LineChart', _index)
      ]),
    ],
  ),
);
Copy the code

Smooth curve effect

Although the basic line graph effect was pretty good, the UI design was smooth curve effect, and the engineer agreed that the curve effect was more elegant, so he decided to challenge himself and implement smooth curve effect himself. Through a layer of source code analysis, finally found that the realization of line map line location, rewriting the implementation can achieve smooth curve effect

line_chart.dart

defaultRenderer: charts.LineRendererConfig( // The configuration of line drawing
  includeArea: true,
  includePoints: true,
  includeLine: true,
  stacked: false,),Copy the code

line_renderer.dart

if(config.includeLine) { ... canvas.drawLine( clipBounds: _getClipBoundsForExtent(line.positionExtent), dashPattern: line.dashPattern, points: line.points, stroke: line.color, strokeWidthPx: line.strokeWidthPx, roundEndCaps: line.roundEndCaps); }}); }});Copy the code

chart_canvas.dart

@override
void drawLine(
   ...
  _linePainter.draw(
      canvas: canvas,
      paint: _paint,
      points: points,
      clipBounds: clipBounds,
      fill: fill,
      stroke: stroke,
      roundEndCaps: roundEndCaps,
      strokeWidthPx: strokeWidthPx,
      dashPattern: dashPattern);
}
Copy the code

Now that we have found the specific entry point for drawing polylines, all that remains is how to draw a smooth curve based on the given data set, and the range of the curve should not exceed the range of the data set. Three curve drawing algorithms were tried before and after, the first two were abandoned because they exceeded the scope of the data set, and the final curve effect was drawn by the third algorithm.

  • Spline interpolation

Spline interpolation is a common interpolation method for obtaining smooth curves in industrial design, and cubic spline is widely used. The algorithm is based on Java cubic spline interpolation and the code is interpolation. Dart

class Interpolation {
  int n;
  List<num> xs;
  List<num> ys;

  bool spInitialized;
  List<num> spY2s;

  Interpolation(List<num> _xs, List<num> _ys) {
    this.n = _xs.length;
    this.xs = _xs;
    this.ys = _ys;
    this.spInitialized = false;
  }

  num spline(num x) {
    if (!this.spInitialized) {
      // Assume Natural Spline Interpolation
      num p, qn, sig, un;
      List<num> us;

      us = new List<num>(n - 1);
      spY2s = new List<num>(n);
      us[0] = spY2s[0] = 0.0;

      for (int i = 1; i <= n - 2; i++) {
        sig = (xs[i] - xs[i - 1]) / (xs[i + 1] - xs[i - 1]);
        p = sig * spY2s[i - 1] + 2.0;
        spY2s[i] = (sig - 1.0) / p;
        us[i] = (ys[i + 1] - ys[i]) / (xs[i + 1] - xs[i]) -
            (ys[i] - ys[i - 1]) / (xs[i] - xs[i - 1]);
        us[i] = (6.0 * us[i] / (xs[i + 1] - xs[i - 1]) - sig * us[i - 1]) / p;
      }
      qn = un = 0.0;

      spY2s[n - 1] = (un - qn * us[n - 2]) / (qn * spY2s[n - 2] + 1.0);
      for (int k = n - 2; k >= 0; k--) {
        spY2s[k] = spY2s[k] * spY2s[k + 1] + us[k];
      }

      this.spInitialized = true;
    }

    int klo, khi, k;
    num h, b, a;

    klo = 0;
    khi = n - 1;
    while (khi - klo > 1) {
      k = (khi + klo) >> 1;
      if (xs[k] > x)
        khi = k;
      else
        klo = k;
    }
    h = xs[khi] - xs[klo];
    if (h == 0.0) {
      throw new Exception('h = = 0.0');
    }
    a = (xs[khi] - x) / h;
    b = (x - xs[klo]) / h;
    return a * ys[klo] +
        b * ys[khi] +
        ((a * a * a - a) * spY2s[klo] + (b * b * b - b) * spY2s[khi]) *
            (h * h) /
            6.0; }}Copy the code

line_painter.dart

/// Draws smooth lines between each point.
void _drawSmoothLine(Canvas canvas, Paint paint, List<Point> points) {
  var interval = 0.1;
  var interpolationPoints = List<Point>();
  for (int k = 0; k < points.length; k++) {
    if ((k + 1) < points.length) {
      num temp = 0;
      while (temp < points[k + 1].x) {
        temp = temp + interval;
        interpolationPoints.add(Point(temp, 0.0)); }}}var tempX = points.map((item) => item.x).toList();
  var tempY = points.map((item) => item.y).toList();
  var ip = Interpolation(tempX, tempY);
  for (int j = 0; j < interpolationPoints.length; j++) {
    interpolationPoints[j] =
        Point(interpolationPoints[j].x, ip.spline(interpolationPoints[j].x));
  }
  interpolationPoints.addAll(points);
  interpolationPoints.sort((a, b) {
    if (a.x == b.x)
      return 0;
    else if (a.x < b.x)
      return - 1;
    else
      return 1;
  });
  final path = new Path();
  path.moveTo(interpolationPoints[0].x.toDouble(), interpolationPoints[0].y.toDouble());
  for (int i = 1; i < interpolationPoints.length; i++) {
    path.lineTo(interpolationPoints[i].x.toDouble(), interpolationPoints[i].y.toDouble());
  }
  canvas.drawPath(path, paint);
}
Copy the code

Final rendering

It looks perfect, but there is a fatal problem. The vertices of the curves can be out of range of the line chart data

  • Bessel curve

Cubic Bezier curve is such a curve, which is a smooth curve drawn according to the coordinates of arbitrary points in four positions. The difficulty is the calculation of two control points. The algorithm smoothly fits broken line segments by referring to Bezier curve, and the code is implemented as follows: Line_Painter

/// Draws smooth lines between each point.
void _drawSmoothLine(Canvas canvas, Paint paint, List<Point> points) {
  var targetPoints = List<Point>();
  targetPoints.add(points[0]);
  targetPoints.addAll(points);
  targetPoints.add(points[points.length - 1]);
  final path = new Path();
  for (int i = 1; i < targetPoints.length - 2; i++) {
    path.moveTo(
        targetPoints[i].x.toDouble(), targetPoints[i].y.toDouble());
    var controllerPoint1 = Point(
      targetPoints[i].x + (targetPoints[i + 1].x - targetPoints[i - 1].x) / 4,
      targetPoints[i].y + (targetPoints[i + 1].y - targetPoints[i - 1].y) / 4,);var controllerPoint2 = Point(
      targetPoints[i + 1].x - (targetPoints[i + 2].x - targetPoints[i].x) / 4,
      targetPoints[i + 1].y - (targetPoints[i + 2].y - targetPoints[i].y) / 4,); path.cubicTo( controllerPoint1.x, controllerPoint1.y, controllerPoint2.x, controllerPoint2.y, targetPoints[i +1].x, targetPoints[i + 1].y);
  }
  canvas.drawPath(path, paint);
}
Copy the code

Smooth curves are also possible, but there is still the problem of out-of-bounds vertices

  • Bessel curves (Formal X)

Since the previous RN project used Victory-native/Victory-Chart, the source code and documentation found that its curve effect implementation relied on D3. curveMonotoneX of D3-SHAp. The algorithm was based on Monotone.js, and the implementation code is as follows:

Note: Since the algorithm needs the current point and the previous two points to draw a curve, a point is artificially added at the end of the data set of broken line points, otherwise the curve drawn will lack the last segment

line_painter.dart

/// Draws smooth lines between each point.
void _drawSmoothLine(Canvas canvas, Paint paint, List<Point> points) {
  var targetPoints = List<Point>();
  targetPoints.addAll(points);
  targetPoints.add(Point(
      points[points.length - 1].x * 2, points[points.length - 1].y * 2));
  var x0,
      y0,
      x1,
      y1,
      t0,
      path = Path();
  for (int i = 0; i < targetPoints.length; i++) {
    var t1;
    var x = targetPoints[i].x;
    var y = targetPoints[i].y;
    if (x == x1 && y == y1) return;
    switch (i) {
      case 0:
        path.moveTo(x, y);
        break;
      case 1:
        break;
      case 2:
        t1 = MonotoneX.slope3(x0, y0, x1, y1, x, y);
        MonotoneX.point(
            path,
            x0,
            y0,
            x1,
            y1,
            MonotoneX.slope2(x0, y0, x1, y1, t1),
            t1);
        break;
      default:
        t1 = MonotoneX.slope3(x0, y0, x1, y1, x, y);
        MonotoneX.point(
            path,
            x0,
            y0,
            x1,
            y1,
            t0,
            t1);
    }
    x0 = x1;
    y0 = y1;
    x1 = x;
    y1 = y;
    t0 = t1;
  }
  canvas.drawPath(path, paint);
}
Copy the code

The final image, the vertices are all points in the line chart data set, perfect!

  • The source code

See GitHub dev at github.com/123lxw123/c…

The copyright of this article belongs to Zaihui RESEARCH and development team, welcome to reprint, reprint please reserve source. @123lxw123