syncfusion / flutter-widgets

Syncfusion Flutter widgets libraries include high quality UI widgets and file-format packages to help you create rich, high-quality applications for iOS, Android, and web from a single code base.
1.6k stars 785 forks source link

i want to create doughnut graph with rounded corner segments #2111

Open unarayan-sf opened 1 month ago

unarayan-sf commented 1 month ago

Use case

In the doughnut graph, we have a corner style property, setting its value gives a circular arch, we want to make only the corner rounded no arch. (see design). I tried making the CustomDoughnutSeries series and extending the doughnut graph but couldn't accomplish it. I need to know how I can approach this issue. any code sample help is appreciated.

9lTNt
class CustomDoughnutSeries<ChartData, String> extends DoughnutSeries {
  CustomDoughnutSeries({
    required List<ChartData> dataSource,
    required ChartValueMapper<dynamic, String> xValueMapper,
    required ChartValueMapper<dynamic, num> yValueMapper,
    required ChartValueMapper<dynamic, Color> pointColorMapper,
    required bool explode,
    required explodeGesture,
    required explodeIndex,
    required explodeOffset,
    required radius,
    required innerRadius,
    required cornerStyle,
    required startAngle,
    required endAngle,
    required pointRenderMode,
  }) : super(
            dataSource: dataSource,
            xValueMapper: xValueMapper,
            yValueMapper: yValueMapper,
            pointColorMapper: pointColorMapper,
            explode: explode,
            explodeGesture: explodeGesture,
            explodeIndex: explodeIndex,
            explodeOffset: explodeOffset,
            radius: radius,
            innerRadius: innerRadius,
            //cornerStyle: cornerStyle,
            startAngle: startAngle,
            endAngle: endAngle,
            pointRenderMode: pointRenderMode,
  explodeAll: true);

  @override
  DoughnutSeriesRenderer createRenderer() {
    return CustomDoughnutSeriesRenderer();
  }
}

class CustomDoughnutSeriesRenderer extends DoughnutSeriesRenderer {
  CustomDoughnutSeriesRenderer() : super();

  @override
  void onPaint(PaintingContext context, Offset offset) {
    super.onPaint(context, offset);

    final Canvas canvas = context.canvas;
    final Paint paint = Paint()
      ..style = PaintingStyle.fill;

    for (final segment in segments) {
      final Path path = Path();
      final List<Offset> points = segment.points;

      if (points.isNotEmpty) {
        final Offset center = Offset(
          (points[0].dx + points[1].dx) / 2,
          (points[0].dy + points[1].dy) / 2,
        );
        final double outerRadius = (points[1] - center).distance;
        final double innerRadius = (points[2] - center).distance;
        final double startAngle = atan2(points[0].dy - center.dy, points[0].dx - center.dx);
        final double endAngle = atan2(points[1].dy - center.dy, points[1].dx - center.dx);
        final double sweepAngle = endAngle - startAngle;

        // Adjust angles to create gaps
        final double gapAngle = .6; // Adjust this value to change the gap size
        final double adjustedStartAngle = startAngle + gapAngle;
        final double adjustedEndAngle = endAngle - gapAngle;
        final double adjustedSweepAngle = adjustedEndAngle - adjustedStartAngle;

        // Draw the main segment with gaps
        path.arcTo(Rect.fromCircle(center: center, radius: outerRadius), adjustedStartAngle, adjustedSweepAngle, false);
        path.arcTo(Rect.fromCircle(center: center, radius: innerRadius), adjustedEndAngle, -adjustedSweepAngle, false);
        path.close();

        paint.color = segment.fillPaint.color;
        canvas.drawPath(path, paint);

        // Draw the rounded corners
        final double cornerRadius = (outerRadius - innerRadius) / 2;
        final Offset startPoint = Offset(
          center.dx + outerRadius * cos(adjustedStartAngle),
          center.dy + outerRadius * sin(adjustedStartAngle),
        );
        final Offset endPoint = Offset(
          center.dx + outerRadius * cos(adjustedEndAngle),
          center.dy + outerRadius * sin(adjustedEndAngle),
        );

        canvas.drawCircle(startPoint, cornerRadius, paint);
        canvas.drawCircle(endPoint, cornerRadius, paint);
      }
    }
  }
}

Proposal

can someone give me an idea of how we can do it? Thank you so much Guys.

Baranibharathip commented 3 weeks ago

Hi @unarayan-sf,

You can achieve your requirement for a doughnut chart with rounded corners and gaps by using the _CustomDoughnutSeries. By updating the calculateRoundedCornerArcPath() method in custom doughnut segment, you can adjust the corners at the start and end of each segment. Finally, initialize the custom doughnut series in the chart using the onCreateRenderer callback to render the customized segment. Please refer to the following code snippet.

Custom doughnut series:

class _CustomDoughnutSeries<_ChartData, int>
    extends DoughnutSeriesRenderer<_ChartData, int> {
  @override
  DoughnutSegment<_ChartData, int> createSegment() {
    return _ColumnCustomPainter();
  }
}

class _ColumnCustomPainter<_ChartData, int>
    extends DoughnutSegment<_ChartData, int> {
  @override
  void onPaint(Canvas canvas) {
    final Paint paint = getFillPaint();
    final num angleDeviation = findAngleDeviation(
        series.currentInnerRadius, series.currentRadius, 360);
    fillPath = calculateRoundedCornerArcPath(
        series.currentInnerRadius,
        series.currentRadius,
        series.center,
        startAngle + angleDeviation,
        endAngle - angleDeviation,
        1);
    canvas.drawPath(fillPath, paint);
  }
}

num findAngleDeviation(num innerRadius, num outerRadius, num totalAngle) {
  final num midRadius = (innerRadius + outerRadius) / 2;
  final num circumference = 2 * pi * midRadius;
  final num rimSize = (innerRadius - outerRadius).abs();
  final num deviation = ((rimSize / 2) / circumference) * 100;
  return (deviation * 360) / 100;
}

Path calculateRoundedCornerArcPath(double innerRadius, double outerRadius,
    Offset center, double startAngle, double endAngle, double gapAngle) {
  final Path path = Path();

  // Adjust the start and end angles by reducing them by the gap angle
  final double adjustedStartAngle = startAngle + gapAngle / 2;
  final double adjustedEndAngle = endAngle - gapAngle / 2;

  // Calculate the start and end points
  final Offset startPoint =
      calculateOffset(adjustedStartAngle, innerRadius, center);
  Offset endPoint = calculateOffset(adjustedStartAngle, outerRadius, center);

  // Move to the starting point
  path.moveTo(startPoint.dx, startPoint.dy);

  // Create rounded corner arc at the starting point
  path.arcToPoint(endPoint,
      radius: Radius.circular((innerRadius - outerRadius).abs() / 2));

  // Draw the outer arc with adjusted angles to create the gap
  path.addArc(
      Rect.fromCircle(center: center, radius: outerRadius),
      degreesToRadians(adjustedStartAngle),
      degreesToRadians(adjustedEndAngle - adjustedStartAngle));

  // Calculate the end point for the inner arc
  final Offset endPointCurve =
      calculateOffset(adjustedEndAngle, innerRadius, center);

  // Create rounded corner arc at the end point
  path.arcToPoint(endPointCurve,
      radius: Radius.circular((innerRadius - outerRadius).abs() / 2));

  // Draw the inner arc to close the shape
  path.arcTo(
      Rect.fromCircle(center: center, radius: innerRadius),
      degreesToRadians(adjustedEndAngle),
      degreesToRadians(adjustedStartAngle) - degreesToRadians(adjustedEndAngle),
      false);

  return path;
}

Offset calculateOffset(double degree, double radius, Offset center) {
  final double radian = degreesToRadians(degree);
  return Offset(
      center.dx + cos(radian) * radius, center.dy + sin(radian) * radius);
}

double degreesToRadians(double deg) => deg * (pi / 180);

Set the custom doughnut series to chart:

SfCircularChart(
       series: <DoughnutSeries<_ChartData, String>>[
          DoughnutSeries<_ChartData, String>(
              dataSource: data,
              xValueMapper: (_ChartData data, _) => data.category,
              yValueMapper: (_ChartData data, _) => data.value,
              innerRadius: "80%",
              onCreateRenderer: (series) {
                return _CustomDoughnutSeries();
              }),
        ],
      ),

Demo: Screenshot 2024-10-29 151925

We have attached the sample below for you reference. Please check and get back to us if your require further assistance.

Sample : GH_2111.zip

Regards, Baranibharathi P.