4

I want to make a chart with a gradient under the highest value, exactly like this :

enter image description here

how can I do that?

Mahdi Dahouei
  • 1,588
  • 2
  • 12
  • 32

2 Answers2

4

You should use CustomPaint.

  1. Create a custom painter that draws the chart line based on chart data:
class CurvedChartPainter extends CustomPainter {
  final List<double> xValues;
  final List<double> yValues;
  final Color color;
  final double strokeWidth;

  CurvedChartPainter({
    @required this.xValues,
    @required this.yValues,
    @required this.strokeWidth,
    this.color,
  });

  @override
  void paint(Canvas canvas, Size size) {
    var paint = Paint();
    paint.color = color ??  Color(0xFFF63E02);
    paint.style = PaintingStyle.stroke;
    paint.strokeWidth = strokeWidth;

    var path = Path();
    if (xValues.length > 1 && yValues.isNotEmpty) {
      final maxValue = yValues.last;
      final firstValueHeight = size.height * (xValues.first / maxValue);
      path.moveTo(0.0, size.height - firstValueHeight);

      final itemXDistance = size.width / (xValues.length - 1);
      for (var i = 1; i < xValues.length; i++) {
        final x = itemXDistance * i;
        final valueHeight = size.height -
            strokeWidth -
            ((size.height -  strokeWidth) * (xValues[i].value / maxValue));
        final previousValueHeight = size.height -
            strokeWidth -
            ((size.height -  strokeWidth) *
                (xValues[i - 1].value / maxValue));
        
        path.quadraticBezierTo(
          x - (itemXDistance / 2) - (itemXDistance / 8),
          previousValueHeight,
          x - (itemXDistance / 2),
          valueHeight + ((previousValueHeight - valueHeight) / 2),
        );
        path.quadraticBezierTo(
          x - (itemXDistance / 2) + (itemXDistance / 8),
          valueHeight,
          x,
          valueHeight,
        );
      }
    }

    canvas.drawPath(path, paint);
  }

  @override
  bool shouldRepaint(CustomPainter oldDelegate) => oldDelegate != this;
}

  1. Create a Container that renders the gradient:
class MyCurvedChart extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Container(
      decoration: BoxDecoration(
        gradient: LinearGradient(
          begin: Alignment.topCenter,
          end: Alignment.bottomCenter,
          colors: [
            Color(0xFFF63E02).withOpacity(0.45),
            Colors.white.withOpacity(0.0),
          ],
        ),
      ),
      width: 200,
      height: 150,
      child: CustomPaint(
        painter: CurvedChartPainter(
          xValues: [
            0.0,
            1.0,
            0.0,
            2.0,
            3.0,
            1.0,
            1.5,
          ],
          yValues: [
            0.0,
            1.0,
            2.0,
            3.0,
            4.0,
          ],
          strokeWidth: 3.0,
        ),
      ),
    );
  }
}

now our output would look like something like this:
enter image description here

  1. Now we should make a CostumClipper that clips the Container exactly like the chart:
class CurvedChartClipper extends CustomClipper<Path> {
  final List<double> xValues;
  final List<double> yValues;
  final double strokeWidth;

  CurvedChartClipper({
    @required this.xValues,
    @required this.yValues,
    @required this.strokeWidth,
  });

  @override
  Path getClip(Size size) {
    var path = Path();
    if (xValues.length > 1 && yValues.isNotEmpty) {
      final maxValue = yValues.last;
      final firstValueHeight = size.height * (xValues.first / maxValue);
      path.moveTo(0.0, size.height - firstValueHeight);

      final itemXDistance = size.width / (xValues.length - 1);
      for (var i = 1; i < xValues.length; i++) {
        final x = itemXDistance * i;
        final valueHeight = size.height -
            strokeWidth -
            ((size.height -  strokeWidth) * (xValues[i].value / maxValue));
        final previousValueHeight = size.height -
            strokeWidth -
            ((size.height -  strokeWidth) *
                (xValues[i - 1].value / maxValue));

        path.quadraticBezierTo(
          x - (itemXDistance / 2) - (itemXDistance / 8),
          previousValueHeight,
          x - (itemXDistance / 2),
          valueHeight + ((previousValueHeight - valueHeight) / 2),
        );
        path.quadraticBezierTo(
          x - (itemXDistance / 2) + (itemXDistance / 8),
          valueHeight,
          x,
          valueHeight,
        );
      }

      path.lineTo(size.width, size.height);
      path.lineTo(0, size.height);
      path.lineTo(0, 0);
    }

    return path;
  }

  @override
  bool shouldReclip(covariant CustomClipper<Path> oldClipper) =>
      oldClipper != this;
}
  1. use ClipPath widget to clip the Container that has the gradient:
class MyCurvedChart extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final xValues = [
      0.0,
      1.0,
      0.0,
      2.0,
      3.0,
      1.0,
      1.5,
    ];
    final yValues = [
      0.0,
      1.0,
      2.0,
      3.0,
      4.0,
    ];
    final stroke = 3.0;

    return ClipPath(
      clipper: CurvedChartClipper(
        xValues: xValues,
        yValues: yValues,
        strokeWidth: stroke,
      ),
      child: Container(
        decoration: BoxDecoration(
          gradient: LinearGradient(
            begin: Alignment.topCenter,
            end: Alignment.bottomCenter,
            colors: [
              Color(0xFFF63E02).withOpacity(0.45),
              Colors.white.withOpacity(0.0),
            ],
          ),
        ),
        width: 200,
        height: 150,
        child: CustomPaint(
          painter: CurvedChartPainter(
            xValues: xValues,
            yValues: yValues,
            strokeWidth: stroke,
          ),
        ),
      ),
    );
  }
}

Now our output will be something like this:
enter image description here

Mahdi Dahouei
  • 1,588
  • 2
  • 12
  • 32
1

Here is my modified version of the code from @Mahdi Dahouei 's answer.

In my version, you don't need a custom clipper or container to add Gradient, I have added a gradient into the custom painter itself as well as labels for both axis and lines for both axes.

(P.S: I wanted the gradient in the whole chart below the line, but if you only want the highest value with gradient then follow @Mahdi Dahouei's Answer)

Preview:

Code of Custom Painter:


import 'package:flutter/material.dart';

// CustomPainter class to draw a curved chart
class CurvedChartPainter extends CustomPainter {
  // Properties to configure the chart
  final List<Map<String, double>> xValues;
  final List<Map<String, double>> yValues;
  final Color? color;
  final double strokeWidth;
  final List<Color> gradientColors;
  final List<double> gradientStops;
  final TextStyle labelTextStyle;

  // Constructor
  CurvedChartPainter({
    required this.xValues,
    required this.yValues,
    required this.strokeWidth,
    this.color,
    this.gradientColors = const [
      Color(0x00F63E02),
      Color(0xFFFFFFFF),
    ],
    this.gradientStops = const [0.0, 1.0],
    this.labelTextStyle = const TextStyle(color: Colors.grey, fontSize: 12),
  });

  // The paint method is called when the custom painter needs to paint
  @override
  void paint(Canvas canvas, Size size) {
    // Set up the paint for the chart line
    var paint = Paint();
    paint.color = color ?? const Color(0xFFF63E02);
    paint.style = PaintingStyle.stroke;
    paint.strokeWidth = strokeWidth;

    // Set up the paint for the chart fill
    var fillPaint = Paint();
    fillPaint.style = PaintingStyle.fill;

    // Set up the paint for the axes
    var axisPaint = Paint()
      ..color = Colors.grey
      ..style = PaintingStyle.stroke
      ..strokeWidth = 1.0;

    // Draw X axis
    canvas.drawLine(
        Offset(0, size.height), Offset(size.width, size.height), axisPaint);

    // Draw Y axis
    canvas.drawLine(const Offset(0, 0), Offset(0, size.height), axisPaint);

    // Create paths for the chart line and fill
    var path = Path();
    var fillPath = Path();

    // Check if there are enough values to draw the chart
    if (xValues.length > 1 && yValues.isNotEmpty) {
      // Calculate some initial values
      final maxValue = yValues.last.values.last;
      final firstValueHeight =
          size.height * (xValues.first.values.first / maxValue);

      // Initialize the paths with the first point
      path.moveTo(0.0, size.height - firstValueHeight);
      fillPath.moveTo(0.0, size.height);
      fillPath.lineTo(0.0, size.height - firstValueHeight);

      // Calculate the distance between each x value
      final itemXDistance = size.width / (xValues.length - 1);

      // Loop through the x values and draw the chart line and fill
      for (var i = 1; i < xValues.length; i++) {
        final x = itemXDistance * i;
        final valueHeight = size.height -
            strokeWidth -
            ((size.height - strokeWidth) *
                (xValues[i].values.elementAt(0) / maxValue));
        final previousValueHeight = size.height -
            strokeWidth -
            ((size.height - strokeWidth) *
                (xValues[i - 1].values.elementAt(0) / maxValue));

        // Draw a quadratic bezier curve between each point
        path.quadraticBezierTo(
          x - (itemXDistance / 2) - (itemXDistance / 8),
          previousValueHeight,
          x - (itemXDistance / 2),
          valueHeight + ((previousValueHeight - valueHeight) / 2),
        );
        path.quadraticBezierTo(
          x - (itemXDistance / 2) + (itemXDistance / 8),
          valueHeight,
          x,
          valueHeight,
        );

        // Draw the fill path using the same quadratic bezier curves
        fillPath.quadraticBezierTo(
          x - (itemXDistance / 2) - (itemXDistance / 8),
          previousValueHeight,
          x - (itemXDistance / 2),
          valueHeight + ((previousValueHeight - valueHeight) / 2),
        );
        fillPath.quadraticBezierTo(
          x - (itemXDistance / 2) + (itemXDistance / 8),
          valueHeight,
          x,
          valueHeight,
        );
      }

      // Close the fill path
      fillPath.lineTo(size.width, size.height);
      fillPath.close();
    }

    // Create a gradient for the fill
    LinearGradient gradient = LinearGradient(
      colors: gradientColors,
      stops: gradientStops,
      begin: Alignment.topCenter,
      end: Alignment.bottomCenter,
    );
    Rect rect = Rect.fromLTWH(0, 0, size.width, size.height);
    fillPaint.shader = gradient.createShader(rect);

    // Draw the fill path with the gradient
    canvas.drawPath(fillPath, fillPaint);

    // Draw the chart line
    canvas.drawPath(path, paint);

    // Draw X axis labels
    for (int i = 0; i < xValues.length; i++) {
      double x = size.width * i / (xValues.length - 1);
      var textPainter = TextPainter(
        text:
            TextSpan(text: xValues[i].keys.elementAt(0), style: labelTextStyle),
        textDirection: TextDirection.ltr,
      );
      textPainter.layout();
      textPainter.paint(
          canvas, Offset(x - textPainter.width / 2, size.height + 2));
    }

    // Draw Y axis labels
    for (int i = 0; i < yValues.length; i++) {
      double y = size.height * i / (yValues.length - 1);
      double labelValue = yValues.last.values.elementAt(0) *
          (yValues.length - i - 1) /
          (yValues.length - 1);
      var textPainter = TextPainter(
        text: TextSpan(
            text: labelValue.toStringAsFixed(0), style: labelTextStyle),
        textDirection: TextDirection.ltr,
      );
      textPainter.layout();
      textPainter.paint(
          canvas, Offset(-textPainter.width - 2, y - textPainter.height / 2));
    }
  }

  // Determine whether the chart should repaint
  @override
  bool shouldRepaint(CustomPainter oldDelegate) => oldDelegate != this;
}

and usage of custom painter in your widget:

import 'curved_chart_painter.dart';

// Create a stateless widget for the custom curved chart
class MyCurvedChart extends StatelessWidget {
  // Constructor
  const MyCurvedChart({super.key});

  // Build method to generate the widget tree
  @override
  Widget build(BuildContext context) {
    // Define the X axis values for the chart
    // String will be text label and double will be value in the Map<String, double>
    final List<Map<String, double>> xValues = [
      {"day 1": 80.0},
      {"day 2": 50.0},
      {"day 3": 30.0},
      {"day 4": 50.0},
      {"day 5": 10.0},
      {"day 6": 0.0},
      {"day 7": 100.0},
    ];

    // Define the Y axis values for the chart
    // String will be text label and double will be value in the Map<String, double>
    final List<Map<String, double>> yValues = [
      {"0": 0.0},
      {"20": 20.0},
      {"40": 40.0},
      {"60": 60.0},
      {"80": 80.0},
      {"100": 100.0},
    ];

    // Define the stroke width for the chart line
    const stroke = 2.0;

    // Return a SizedBox to limit the size of the chart
    return SizedBox(
      width: MediaQuery.of(context).size.width * 0.8,
      height: MediaQuery.of(context).size.width * 0.6,
      // Use CustomPaint to draw the curved chart
      child: CustomPaint(
        painter: CurvedChartPainter(
          color: Colors.green, // Set the color of the chart line
          yValues: yValues, // Pass the Y axis values
          strokeWidth: stroke, // Set the stroke width
          xValues: xValues, // Pass the X axis values
          gradientColors: [
            // Define the gradient colors for the chart fill
            Colors.green.withAlpha(100),
            const Color(0xFFFFFFFF),
          ],
        ),
      ),
    );
  }
}

Kamal Panara
  • 482
  • 7
  • 16