Flutter - Canvas custom curve chart

development background

The company's functional requirements are developed; it is required to implement UI such as graphs and scales through the Flutter control Canvas;

renderings
Curve rendering

  1. The first step is to realize the coordinate system;

Realize the coordinate system, four points in the upper left and lower right;


  ///原点坐标
  Offset? pointOrigin;

  ///原点顶部左边坐标
  Offset? pointTopLeft;

  ///原点顶部右边坐标
  Offset? pointTopRight;

  ///原点底部右边坐标
  Offset? pointBottomRight;

  ///画布的坐标系的Rect
  Rect? paintRect;
  
  ///1、初始化画布四个点
  initPoint() {
    pointOrigin = fracturingModel.pointOrigin;
    pointTopLeft = fracturingModel.pointTopLeft;
    pointTopRight = fracturingModel.pointTopRight;
    pointBottomRight = fracturingModel.pointBottomRight;
    paintRect = fracturingModel.paintRect;
  }

  1. The second step is to implement the top type identification UI;

Effect picture
top logo
What needs to be noted here is the **drawText()** method, and the implementation method will be posted later;


  ///2、顶部类型样式
  void initDrawTopText() {
    ///1.拿到JSON数据
    var fracturingMaxList = fracturingModel.fracturingsInfoList;
    var fontWidth = 0.0;
    var length = fracturingMaxList.length;

    ///2.算出文字的宽度
    for (var i = 0; i < length; i++) {
      var info = fracturingMaxList[i];
      Size textSize = drawTextBoxSize(info.paramName, 10.0, 'typeface');
      fontWidth += (textSize.width + space + rectWidth + 2);
    }

    ///3.算出总文字宽度的中心点,并从此点绘制出文本跟颜色标识
    var startX = (width - fontWidth) / 2;
    for (var i = 0; i < length; i++) {
      paints.style = PaintingStyle.fill;
      var info = fracturingMaxList[i];

      ///3.1点击选中,是否显示该条曲线
      if (info.isShow) {
        paints.color = ColorsUtils.hexToColor(info.curveColorPlus!);
      } else {
        paints.color = Colors.grey;
      }

      ///3.2计算颜色标识的矩形宽度
      var rect = Rect.fromLTWH(startX, 0, rectWidth, rectHeight);
      ctx.drawRect(rect, paints);

      ///3.3计算文字的起始点
      startX += rectWidth;

      ///3.4绘制文字
      Size drawSize = drawText(info.paramName, startX + 2, 5.0, 'typeface',
          10.0, paints.color, 'left', 'middle');

      ///3.5计算颜色标识与文本的绘制矩形,后期做点击事件的功能
      var rects = Rect.fromLTWH(
          startX - rectWidth, 0, rectWidth + drawSize.width, rectHeight);
      listRect.add(rects);
      startX += drawSize.width + space;
    }
  }

  1. draw grid from four points

renderings
insert image description here

  ///3、绘制网格
  void initDrawLine() {
    paints.color = Colors.grey;
    ///左上y值;
    var y = pointTopLeft!.dy;
    ///左上x值;
    var x = pointTopLeft!.dx;
    for (var i = 0; i < 11; i++) {
      ctx.drawLine(Offset(x, pointTopRight!.dy),
          Offset(x, height - marginBottom + 10.0), paints);
      ctx.drawLine(
          Offset(marginLeft, y), Offset(width - marginRight, y), paints);
      y += averageHeight;
      x += averageWidth;
    }
  }

  1. draw bottom tick

See the third step for the effect picture

  ///5、底部刻度道
  initDrawBottomScale() {
    paints.strokeWidth = 1.0;
    var scaleHeight = 8;
    var paintWidth = width - marginRight - marginLeft;
    var space = paintWidth / 10;
    var y = pointOrigin!.dy;
    var x = marginLeft;
    for (var i = 0; i < fracturingModel.bottomScaleList.length; i++) {
      drawScale(x, y, x, y + scaleHeight);
      drawText(fracturingModel.bottomScaleList[i].toStringAsFixed(0), x,
          y + scaleHeight, 's', 10.0, null, 'center', 'top');
      x = x + space;
    }
  }
  1. Draw left and right tick marks

renderings
left and right scale

  ///6、绘制左侧刻度道
  initDrawLeftRightScale() {
    var fracturingMaxList = fracturingModel.fracturingsInfoList;
    ///左边x轴绘制起点
    var leftX = pointOrigin!.dx - space;
    ///右边x轴绘制起点
    var rightX = width - marginRight + space;
    ///总共有多少条刻度
    var length = fracturingMaxList.length;
    var even = (length / 2).round();
    ///判断奇偶数,根据它来判断左右需要绘画的刻度列数
    if (!MathUtil.isEven(length)) {
      even -= 1;
    }
    for (var i = 0; i < length; i++) {
      var maxData = fracturingMaxList[i];
      if (i < even) {
        var y = pointOrigin!.dy;
        var yyText = 0.0;
        Size? textSize;
        var textWidth = 0.0;
        for (var j = 0; j < 11; j++) {
          textSize = drawText(
              Utils().formatNumber(yyText),
              leftX,
              y,
              's',
              10.0,
              ColorsUtils.hexToColor(maxData.curveColorPlus!),
              'right',
              'middle');
          y -= averageHeight;
          yyText += (maxData.maxValue! / 10);
          if (textWidth < textSize!.width) {
            textWidth = textSize.width;
          }
        }
        leftX -= (textWidth + space);
      } else {
        var y = pointOrigin!.dy;
        var yyText = 0.0;
        Size? textSize;
        var textWidth = 0.0;
        for (var j = 0; j < 11; j++) {
          textSize = drawText(
              Utils().formatNumber(yyText),
              rightX,
              y,
              's',
              10.0,
              ColorsUtils.hexToColor(maxData.curveColorPlus!),
              'left',
              'middle');
          y -= averageHeight;
          yyText += (maxData.maxValue! / 10);
          if (textWidth < textSize!.width) {
            textWidth = textSize.width;
          }
        }
        rightX += (textWidth + space);
      }
    }
  }

  1. draw a graph

renderings
insert image description here

  void initDrawYYPointLine() {
    ctx.save();
    ///先绘制区域
    Rect rect = Rect.fromLTWH(
        pointOrigin!.dx,
        pointTopLeft!.dy,
        pointTopRight!.dx - pointTopLeft!.dx,
        pointBottomRight!.dy - pointTopRight!.dy);
    ///裁剪区域以外的部分
    ctx.clipRect(rect);
    ///绘制每条曲线
    for (var points in fracturingModel.listPoints) {
      drawLinePoints(points);
    }
    ctx.restore();
  }

  drawLinePoints(ListPoints points) {
    if (points.isShow) {
      paints.strokeWidth = 1.0;
      paints.style = PaintingStyle.stroke;
      paints.strokeCap = StrokeCap.butt;
      paints.strokeJoin = StrokeJoin.round;
      paints.color = points.color ?? Colors.black;
      ctx.drawPoints(PointMode.polygon, points.offsetZommScaleList, paints);
    }
  }
  1. Click to view the detailed data of this point

renderings
insert image description here

  drawDashLine([fromX, fromY, toX, toY, gap]) {
    var path = Path();
    path.reset();
    path.moveTo(fromX, fromY);
    path.lineTo(toX, toY);
    paints.strokeWidth = 1.0;
    var paint = Paint()
      ..strokeWidth = 1.0
      ..color = Colors.black
      ..style = PaintingStyle.stroke;
    ctx.drawPath(getDashLine(path, gap, 5.0), paint);
    drawPointTextInfo(fromX, toX);
  }

  Path getDashLine([path, dottedLength, dottedGap]) {
    Path targetPath = Path(); //虚线Path
    for (PathMetric metrice in path.computeMetrics()) {
      double distance = 0;
      bool isDrawDotted = true;
      while (distance < metrice.length) {
        if (isDrawDotted) {
          Path extractPath =
              metrice.extractPath(distance, distance + dottedLength);
          targetPath.addPath(extractPath, Offset.zero);
          distance += dottedLength;
        } else {
          distance += dottedGap;
        }
        isDrawDotted = !isDrawDotted;
      }
    }
    return targetPath;
  }
  
  ///绘制点击之后每个点的详细信息
  drawPointTextInfo(fromX, toX) {
    var textWidth = 0.0;
    var textHeight = 0.0;
    for (var i = 0; i < fracturingModel.fracturingsInfoList.length; i++) {
      var itemInfo = fracturingModel.fracturingsInfoList[i];
      Size textSize;
      if (i == 0) {
        textSize = drawTextBoxSize(
            '入库时间:${itemInfo.warehousingTime}  ', 10.0, 'typeface');
        textHeight += textSize.height + 5;
      } else {
        textSize = drawTextBoxSize(
            '${itemInfo.paramName}:${itemInfo.detailValues}  ',
            10.0,
            'typeface');
      }
      textHeight += textSize.height + 5;
      if (textWidth < textSize.width) {
        textWidth = textSize.width;
      }
    }
    textWidth += 10;
    var pointHeight = pointBottomRight!.dy - pointTopRight!.dy;
    var bottom = (pointHeight - textHeight) / 2;
    var top = bottom + textHeight;
    var paint = Paint();
    paint.color = Colors.black54;
    paint.style = PaintingStyle.fill;

    var l = 0.0;
    var t = 0.0;
    var r = 0.0;
    var b = 0.0;

    ///1.说明右边距离不够
    if (pointTopRight!.dx - fromX < textWidth) {
      l = fromX - textWidth;
      r = fromX;
    } else {
      l = fromX;
      r = fromX + textWidth;
    }
    t = getY(top);
    b = getY(bottom);

    RRect rrect = RRect.fromLTRBR(l, t, r, b, const Radius.circular(5.0));
    ctx.drawRRect(rrect, paint);
    var y = getY(top - 10);
    for (var i = 0; i < fracturingModel.fracturingsInfoList.length; i++) {
      var itemInfo = fracturingModel.fracturingsInfoList[i];
      if (i == 0) {
        Size size = drawText('入库时间:${itemInfo.warehousingTime}', rrect.left + 5,
            y, 'typeface', 10.0, Colors.white, 'left', 'middle');
        y += size.height + 5;
      }
      paint.color = ColorsUtils.hexToColor(itemInfo.curveColorPlus!);
      ctx.drawCircle(Offset(rrect.left + 10, y), 5, paint);
      Size textSize = drawText('${itemInfo.paramName}:${itemInfo.detailValues}',
          rrect.left + 20, y, 'typeface', 10.0, Colors.white, 'left', 'middle');
      y += textSize.height + 5;
    }
  }

  getX(x) {
    return pointOrigin!.dx + x;
  }

  getY(y) {
    return pointOrigin!.dy - y;
  }


When handling click events, you need to pay attention. According to the click coordinate Offset, use the paintRect!.contains(localPosition) method to judge whether it is within this range, and then do the corresponding UI drawing operation;

  ///返回点击类型 1点击曲线图 2.点击顶部标识
  onHitTest(Offset localPosition) {
    ///画布类型
    if (paintRect != null && paintRect!.contains(localPosition)) {
      return {'type': 'curveGraph', 'position': ''};
    } else {
    ///顶部标识类型
      for (var i = 0; i < listRect.length; i++) {
        Rect rect = listRect[i];
        if (rect.contains(localPosition)) {
          return {'type': 'topTypeGraph', 'position': i};
        }
      }
    }
    return {'type': 'cancel', 'position': ''};
  }

After clicking, get the type data and do a series of logical operations

  void onTapDown(detail, map) {
    if (detail != null) {
      var type = map['type'];
      if (type == 'curveGraph') {
        ///点击的是曲线图
        localPosition = detail;
        var listPoint = listPoints[0];
        var length = listPoint.offsetList.length;
        var startOffset = listPoint.offsetList[0];
        var endOffset = listPoint.offsetList[length - 1];
        if (detail.dx > startOffset.dx || detail.dx < endOffset.dx) {
          ///点击的x点
          var x = double.parse(getTimeX(detail.dx).toStringAsFixed(4));
          var fracturingList = fracturingsInfoList;
          for (var i = 0; i < fracturingList.length; i++) {
            var fracturingMaxList = fracturingsInfoList[i];
            var itemList = fracturingList[i].listFracturing;
            var info = 0.0;
            var sjList = sjMaxList;
            var time = '';
                 ///通过二分查找到相应的索引,进行获取详细的数据信息,进行展示
            var index =
                MathUtil.binarySearchNums(sjList, 0, sjList.length - 1, x);
            if (index == -1) {
              info = 0.0;
              time = '';
            } else if (index == 0 || index == fracturingList.length - 1) {
              info = itemList[index];
              time = cjsjList[index];
            } else {
              time = cjsjList[index];
              var x0 = sjList[index];
              var x1 = sjList[index + 1];
              var y0 = itemList[index];
              var y1 = itemList[index + 1];
              var k = (x - x0) / (x1 - x0);
              var y = y0 + (y1 - y0) * k;
              info = y;
            }
            fracturingMaxList.warehousingTime = time;
            fracturingMaxList.detailValues = info.toStringAsFixed(2);
          }
        }
      } else if (type == 'topTypeGraph') {
      ///改变数据源重新渲染,是否绘制相对应的曲线
        var position = map['position'];
        fracturingsInfoList[position].isShow =
            !fracturingsInfoList[position].isShow;
        listPoints[position].isShow = !listPoints[position].isShow;
      }
    }
  }
  1. Curve zoom function

It is an extended function;
it needs to be referenced in the file pubspec.yaml , add syncfusion_flutter_sliders: ^20.1.57
Pay attention to three states,
1. When dragging the starting point, you need to convert the ratio of the zoom ratio to the x-axis;
2. Drag the end point , you need to convert the displacement ratio of the x-axis;
3. When dragging in the interval, you need to convert the ratio of the zoom ratio to the x-axis;

  SfRangeValues onChangedSlide(SfRangeValues values, SfRangeValues oldSfRange) {
    bottomScaleList.clear();

    ///刻度总宽度
    var totalWidth = values.end - values.start;
    var equalParts = totalWidth ~/ 10;

    ///起始位置
    var start = values.start;

    ///结束位置
    var end = values.end;
    zommScale = sjMax / totalWidth;
    var newMax = sjMax / zommScale;
    equalParts = newMax / 10;

    ///重新换算x轴比例
    ratioX = getRatioX(newMax);
    var oldWith = (width - marginLeft - marginRight);
    var newWidth = oldWith * zommScale;
    bottomScaleList.add(start);
    for (var i = 0; i < 10; i++) {
      start += equalParts;
      bottomScaleList.add(start);
    }
    if (oldSfRange.start == values.start) {
      ///说明是拖动的结束点
      if (values.start != 0) {
        translateX = values.start / sjMax * newWidth;
      } else {
        translateX = 0;
      }
      for (var points in listPoints) {
        points.offsetZommScaleList = List.from(points.offsetList);
        for (var i = 0; i < points.offsetZommScaleList.length; i++) {
          var d = (points.offsetZommScaleList[i].dx - marginLeft) * zommScale +
              marginLeft;
          points.offsetZommScaleList[i] =
              Offset(d, points.offsetZommScaleList[i].dy);
          points.offsetZommScaleList[i] =
              points.offsetZommScaleList[i].translate(-translateX, 0.0);
        }
      }
    } else if (oldSfRange.end == values.end) {
      /// 说明是拖动的开始点
      if (values.end != 0) {
        translateX = values.start / sjMax * newWidth;
      } else {
        translateX = (sjMax - totalWidth) / sjMax * newWidth;
      }
      for (var points in listPoints) {
        points.offsetZommScaleList = List.from(points.offsetList);
        for (var i = 0; i < points.offsetZommScaleList.length; i++) {
          var d = (points.offsetZommScaleList[i].dx - marginLeft) * zommScale +
              marginLeft;
          points.offsetZommScaleList[i] =
              Offset(d, points.offsetZommScaleList[i].dy);
          points.offsetZommScaleList[i] =
              points.offsetZommScaleList[i].translate(-translateX, 0.0);
        }
      }
    } else if (oldSfRange.start != values.start &&
        oldSfRange.end != values.end) {
      print('说明是拖动的整条线');
      translateX = values.start / sjMax * newWidth;
      for (var points in listPoints) {
        points.offsetZommScaleList = List.from(points.offsetList);
        for (var i = 0; i < points.offsetZommScaleList.length; i++) {
          var d = (points.offsetZommScaleList[i].dx - marginLeft) * zommScale +
              marginLeft;
          points.offsetZommScaleList[i] =
              Offset(d, points.offsetZommScaleList[i].dy);
          points.offsetZommScaleList[i] =
              points.offsetZommScaleList[i].translate(-translateX, 0.0);
        }
      }
    }
    return values;
  }

renderings
insert image description here

Project demo address: https://github.com/z244370114/flutter_demo

Guess you like

Origin blog.csdn.net/u013290250/article/details/125368760