Series customization in Flutter Circular Charts (SfCircularChart)
7 Aug 202524 minutes to read
Animation
SfCircularChart provides animation support for the series. Series will be animated while rendering. Animation is enabled by default, you can also control the duration of the animation using animationDuration property. You can disable the animation by setting 0 value to that property.
@override
    Widget build(BuildContext context) {
        return Scaffold(
            body: Center(
                child: Container(
                    child: SfCircularChart(
                        series: <CircularSeries<ChartData,String>>[
                            // Render pie chart
                            PieSeries<ChartData, String>(
                                dataSource: chartData,
                                pointColorMapper:(ChartData data,  _) => data.color,
                                xValueMapper: (ChartData data, _) => data.x,
                                yValueMapper: (ChartData data, _) => data.y,
                                animationDuration: 1000
                            )
                        ]
                    )
                )
            )
        );
    }Animation delay
The animationDelay property is used to specify the delay duration of the series animation. This takes milliseconds value as input. By default, the series will get animated for the specified duration. If animationDelay is specified, then the series will begin to animate after the specified duration. Defaults to 0.
@override
    Widget build(BuildContext context) {
        List<ChartData> data = [
            ChartData('Jan', 35),
            ChartData('Feb', 28),
            ChartData('Mar', 38),
            ChartData('Apr', 32),
            ChartData('May', 40)
        ];
        return Center(
            child: SfCircularChart(
                series: <CircularSeries<ChartData, String>>[
                    PieSeries<ChartData, String>(
                        dataSource: data,
                        animationDuration: 4500,
                        animationDelay: 2000,
                        xValueMapper: (ChartData data, _) => data.x,
                        yValueMapper: (ChartData data, _) => data.y,
                    )
                ]
            ),
        );
    }
    class ChartData {
      ChartData(this.x, this.y);
      final String x;
      final double y;
    }Dynamic animation
SfCircularChart also provide the dynamic animation support for the series. The series can be dynamically added to the charts, it will animate by setting the timer value. when you set the animationDuration value to 0, the series won’t be animated.
Color Palette
SfCircularChart provides support for color palette property called palette for the data points in the chart series. If the series color is not specified, then the series will be rendered with appropriate palette color. Ten colors are available by default.
@override
    Widget build(BuildContext context) {
        return Scaffold(
            body: Center(
                child: Container(
                    child: SfCircularChart(
                        palette: <Color>[Colors.amber, Colors.brown, Colors.green, Colors.redAccent, Colors.blueAccent, Colors.teal],
                        series: <CircularSeries<ChartData,String>>[
                            // Render pie chart
                            PieSeries<ChartData, String>(
                                dataSource: chartData,
                                pointColorMapper:(ChartData data,  _) => data.color,
                                xValueMapper: (ChartData data, _) => data.x,
                                yValueMapper: (ChartData data, _) => data.y,
                            )
                        ]
                    )
                )
            )
        );
    }
Color mapping for data points
The pointColorMapper property is used to map the color field from the data source.
@override
    Widget build(BuildContext context) {
        List<ChartData> chartData = <ChartData>[
            ChartData('Rent', 1000,Colors.teal),
            ChartData('Food', 2500,Colors.lightBlue),
            ChartData('Savings', 760,Colors.brown),
            ChartData('Tax', 1897,Colors.grey),
            ChartData('Others', 2987,Colors.blueGrey)
        ];
        return Scaffold(
            body: Center(
                child: Container(
                    child: SfCircularChart(
                        series: <PieSeries<ChartData, String>>[
                            PieSeries<ChartData, String>(
                                dataSource: chartData,
              xValueMapper: (ChartData data, _) => data.x,
              yValueMapper: (ChartData data, _) => data.y,
              //map Color for each dataPoint datasource.
              pointColorMapper: (ChartData data,_) => data.color,
                            )
                        ]
                    )
                )
            )
        );
    }
Gradient and image shader
The onCreateShader callback is used to fill the circular chart series data points with gradient and image shader. This callback is called once while rendering
the data points and legend.
Note: All the data points of the circular chart are considered together as a single segment and the shader is applied commonly.
Gradient fill
The data points of pie, doughnut and radial bar charts can be filled with three types of Gradient such as Gradient.linear, Gradient.sweep and Gradient.radial. All the data points in the circular chart are together considered as a single segment and the shader is applied commonly.
Linear gradient
List<Color> colors = <Color>[
      const Color.fromRGBO(75, 135, 185, 1),
      const Color.fromRGBO(192, 108, 132, 1),
      const Color.fromRGBO(246, 114, 128, 1),
      const Color.fromRGBO(248, 177, 149, 1),
      const Color.fromRGBO(116, 180, 155, 1)
    ];
    List<double> stops = <double>[
      0.2,
      0.4,
      0.6,
      0.8,
      1,
    ];
    @override
    Widget build(BuildContext context) {
        return Container(
            child: SfCircularChart(
                onCreateShader: (ChartShaderDetails chartShaderDetails) {
                    return Gradient.linear(chartShaderDetails.outerRect.topRight,
                        chartShaderDetails.outerRect.centerLeft, colors, stops);
                },
                series: <CircularSeries<_ChartData, String>>[
                    PieSeries<_ChartData, String>(
                        dataSource: chartData,
                        xValueMapper: (_ChartData data, _) => data.x,
                        yValueMapper: (_ChartData data, _) => data.y,
                    )
                ]
            ));
    }
Sweep gradient
@override
    Widget build(BuildContext context) {
        return Container(
            child: SfCircularChart(
                onCreateShader: (ChartShaderDetails chartShaderDetails) {
                    return Gradient.sweep(
                        chartShaderDetails.outerRect.center,
                        colors,
                        stops,
                        TileMode.clamp,
                        _degreeToRadian(0),
                        _degreeToRadian(360),
                        _resolveTransform(chartShaderDetails.outerRect, TextDirection.ltr)
                    );
                },
                series: <CircularSeries<_ChartData, String>>[
                    RadialBarSeries<_ChartData, String>(
                        dataSource: chartData,
                        xValueMapper: (_ChartData data, _) => data.x,
                        yValueMapper: (_ChartData data, _) => data.y,
                    )
                ]
            )
        );
    }
    // Rotate the sweep gradient according to the start angle 
    Float64List _resolveTransform(Rect bounds, TextDirection textDirection) {
        final GradientTransform transform = GradientRotation(_degreeToRadian(-90));
        return transform.transform(bounds, textDirection: textDirection)!.storage;
    }
    // Convert degree to radian
    double _degreeToRadian(int deg) => deg * (3.141592653589793 / 180);
Radial gradient
@override
    Widget build(BuildContext context) {
        return Container(
            child: SfCircularChart(
                onCreateShader: (ChartShaderDetails chartShaderDetails) {
                    return Gradient.radial(
                        chartShaderDetails.outerRect.center,
                        chartShaderDetails.outerRect.right - chartShaderDetails.outerRect.center.dx,
                        colors,
                        stops
                    );
                },
                series: <CircularSeries<_ChartData, String>>[
                    DoughnutSeries<_ChartData, String>(
                        dataSource: chartData,
                        xValueMapper: (_ChartData data, _) => data.x,
                        yValueMapper: (_ChartData data, _) => data.y,
                    )
                ]
            ));
    }
Image fill
The data points of pie, doughnut and radial bar charts can also be filled with image by returning ImageShader with required parameters.
/// Package import
    import 'dart:async';
    import 'dart:ui' as ui;
    ui.Image? image;
    Future<void> getImage() async {
        const ImageProvider imageProvider = AssetImage('assets/apple.png');
        final Completer<ImageInfo> completer = Completer<ImageInfo>();
        imageProvider.resolve(const ImageConfiguration()).addListener(ImageStreamListener((ImageInfo info, bool _) {
            completer.complete(info);
        }));
        final ImageInfo imageInfo = await completer.future;
        image = imageInfo.image;
        isLoadedImage = true;
    }
    @override
    Widget build(BuildContext context) {
        getImage();
        return Container(
            child: SfCircularChart(
                onCreateShader: (ChartShaderDetails chartShaderDetails) {
                    return ImageShader(
                                ui.image!,
                                TileMode.mirror,
                                TileMode.mirror,
                                Matrix4.identity().scaled(0.4).storage
                    );
                },
                series: <CircularSeries<_ChartData, String>>[
                    PieSeries<_ChartData, String>(
                        dataSource: chartData,
                        xValueMapper: (_ChartData data, _) => data.x,
                        yValueMapper: (_ChartData data, _) => data.y,
                    )
                ]
            ));
    }
See Also
Shader mapping for data points
The pointShaderMapper property is used to map the shader field from the chart data source. You can map different Gradient types and ImageShader for individual data points using this mapper callback.
/// Package import
    import 'dart:async';
    import 'dart:ui' as ui;
    ui.Image? image1;
    ui.Image? image2;
    ui.Image? image3;
    ui.Image? image4;
    // To get the images from asset folder
    void getImage() async {
        final Completer<ImageInfo> completer = Completer();
        final ImageProvider imageProvider = AssetImage('images/apple.png');
        imageProvider.resolve(const ImageConfiguration()).addListener(ImageStreamListener((ImageInfo info, bool _) async {
        completer.complete(info);
            final ImageInfo imageInfo = await completer.future;
            image1 = imageInfo.image;
        }));
        final Completer<ImageInfo> completer1 = Completer();
        final ImageProvider imageProvider1 = AssetImage('images/orange.png');
        imageProvider1.resolve(const ImageConfiguration()).addListener(ImageStreamListener((ImageInfo info, bool _) async {
            completer1.complete(info);
            final ImageInfo imageInfo1 = await completer1.future;
            image2 = imageInfo1.image;
        }));
        final Completer<ImageInfo> completer2 = Completer();
        final ImageProvider imageProvider2 = AssetImage('images/pears.png');
        imageProvider2.resolve(const ImageConfiguration()).addListener(ImageStreamListener((ImageInfo info, bool _) async {
            completer2.complete(info);
            final ImageInfo imageInfo2 = await completer2.future;
            image3 = imageInfo2.image;
        }));
        final Completer<ImageInfo> completer3 = Completer();
        final ImageProvider imageProvider3 = AssetImage('images/other_fruits.png');
        imageProvider3.resolve(const ImageConfiguration()).addListener(ImageStreamListener((ImageInfo info, bool _) async {
            completer3.complete(info);
            final ImageInfo imageInfo4 = await completer3.future;
            image4 = imageInfo4.image;
            if (mounted) {
                setState(() {});
            }
        }));
    }
    Widget? renderWidget;
    @override
    Widget build(BuildContext context) {
        getImage();
        if (image1 != null && image2 != null && image3 != null && image4 != null) {
            renderWidget = SfCircularChart(
                title: ChartTitle(text: 'Sales comparison of fruits in a shop'),
                series: <PieSeries<_ChartShaderData, String>>[
                    PieSeries<_ChartShaderData, String>(
                        dataSource: <_ChartShaderData>[
                            _ChartShaderData(
                                'Apple',
                                25,
                                '25%',
                                ui.ImageShader(
                                    image1!,
                                    TileMode.repeated,
                                    TileMode.repeated,
                                    Matrix4.identity().scaled(0.5).storage,
                                ),
                            ),
                            _ChartShaderData(
                                'Orange',
                                35,
                                '35%',
                                ui.ImageShader(
                                    image2!,
                                    TileMode.repeated,
                                    TileMode.repeated,
                                    Matrix4.identity().scaled(0.6).storage,
                                ),
                            ),
                            _ChartShaderData(
                                'Pears',
                                22,
                                '22%',
                                ui.ImageShader(
                                    image3!,
                                    TileMode.repeated,
                                    TileMode.repeated,
                                    Matrix4.identity().scaled(0.6).storage,
                                ),
                            ),
                            _ChartShaderData(
                                'Others',
                                18,
                                '18%',
                                ui.ImageShader(
                                    image4!,
                                    TileMode.repeated,
                                    TileMode.repeated,
                                    Matrix4.identity().scaled(0.5).storage,
                                ),
                            ),
                        ],
                        strokeColor: Colors.black.withOpacity(0.5),
                        strokeWidth: 1.5,
                        explodeAll: true,
                        explodeOffset: '3%',
                        explode: true,
                        xValueMapper: (_ChartShaderData data, _) => data.x,
                        yValueMapper: (_ChartShaderData data, _) => data.y,
                        dataLabelMapper: (_ChartShaderData data, _) => data.text,
                        // mapped the shader data from the chart's data source
                        pointShaderMapper: (_ChartShaderData data, _, Color color, Rect rect) => data.shader,
                        radius: '83%',
                    ),
                ],
            );
        } else {
            getImage();
            renderWidget = Center(child: CircularProgressIndicator());
        }
        return Scaffold(
            body: renderWidget!
        );
    }
    class _ChartShaderData {
        _ChartShaderData(this.x, this.y, this.text, this.shader);
        final String x;
        final num y;
        final String text;
        final Shader shader;
    }
Point render mode
The pointRenderMode property is used to define the painting mode for the data points. The data points in the pie and doughnut chart can be filled either with solid colors or with sweep gradient by using this property. This property is not applicable for RadialBarSeries.
- 
    If PointRenderMode.segmentis specified, the data points are filled with solid colors from the palette or with the colors mentioned inpointColorMapperproperty.
- 
    If PointRenderMode.gradientis specified, a sweep gradient is formed with the solid colors and fills the data points.
Note: This property is applicable only if the
onCreateShaderandpointShaderMapperare null.
@override
    Widget build(BuildContext context) {
        getImage();
        return Container(
            child: SfCircularChart(
                series: <CircularSeries<_ChartData, String>>[
                    PieSeries<_ChartData, String>(
                        dataSource: chartData,
                        // Sweep gradient will be formed with default palette colors.
                        pointRenderMode: PointRenderMode.gradient,
                        xValueMapper: (_ChartData data, _) => data.x,
                        yValueMapper: (_ChartData data, _) => data.y,
                    )
                ]
            )
        );
    }
Empty points
The data points that has null value are considered as empty points. Empty data points are ignored and not plotted in the chart. By using emptyPointSettings property in series, you can decide the action taken for empty points. Available modes are EmptyPointMode.gap, EmptyPointMode.zero, EmptyPointMode.drop and EmptyPointMode.average. Default mode of the empty point is EmptyPointMode.gap.
@override
    Widget build(BuildContext context) {
        
         final List<ChartData> chartData = [
            ChartData('David', null),
            ChartData('Steve', 38),
            ChartData('Jack', 34),
            ChartData('Others', 52)
        ];
        return Scaffold(
            body: Center(
                child: SfCircularChart(
                    series: <CircularSeries<ChartData,String>>[
                        // Render pie chart
                        PieSeries<ChartData, String>(
                            dataSource: chartData,
                            dataLabelSettings: DataLabelSettings(isVisible:true),
                            emptyPointSettings:
                      EmptyPointSettings(mode: EmptyPointMode.average),
                            pointColorMapper:(ChartData data,  _) => data.color,
                            xValueMapper: (ChartData data, _) => data.x,
                            yValueMapper: (ChartData data, _) => data.y,)
                    ]
                )
            )
        );
    }
Empty point customization
Specific color for empty point can be set by color property in emptyPointSettings. The borderWidth property is used to change the stroke width of the empty point and borderColor is used to change the stroke color of the empty point.
@override
    Widget build(BuildContext context) {
        final List<ChartData> chartData = [
        ChartData('David', null),
        ChartData('Steve', 38),
        ChartData('Jack', 34),
        ChartData('Others', 52)
        ];
        return Scaffold(
            body: Center(
                child: Container(
                    child: SfCircularChart(
                        series: <CircularSeries<ChartData, String>>[
                            PieSeries<ChartData, String>(
                            dataSource: chartData,
                            dataLabelSettings: DataLabelSettings(isVisible: true),
                            emptyPointSettings: EmptyPointSettings(
                                mode: EmptyPointMode.average,
                                color: Colors.red,
                                borderColor: Colors.black,
                                borderWidth: 2),
                            xValueMapper: (ChartData data, _) => data.x,
                            yValueMapper: (ChartData data, _) => data.y)
                        ]
                    )
                )
            )
        );
    }
Sorting
The chart’s data source can be sorted using the sortingOrder and sortFieldValueMapper properties of series. The sortingOrder property specifies the data points in the series can be sorted in SortingOrder.ascending or SortingOrder.descending order. The data points will be rendered in the specified order if sortingOrder is set to SortingOrder.none. The sortFieldValueMapper specifies the field in the data source, which is considered for sorting the data points.
@override
    Widget build(BuildContext context) {
        
        final List<ChartData> chartData = [
            ChartData('David', 25),
            ChartData('Steve', 38),
            ChartData('Jack', 34),
            ChartData('Others', 52)
        ];
        return Scaffold(
            body: Center(
                child: Container(
                    child: SfCircularChart(
                      series: <CircularSeries<ChartData,String>>[
                        // Render pie chart
                        PieSeries<ChartData, String>(
                            dataSource: chartData,
                            dataLabelSettings: DataLabelSettings(isVisible:true),
                            sortingOrder: SortingOrder.ascending,
                            sortFieldValueMapper: (ChartData data, _) => data.x,
                            pointColorMapper:(ChartData data,  _) => data.color,
                            xValueMapper: (ChartData data, _) => data.x,
                            yValueMapper: (ChartData data, _) => data.y,
                            animationDuration: 1000
                        )]))));
    }