library flutter_radar_chart; import 'dart:math' as math; import 'package:flutter/material.dart'; import 'dart:math' show pi, cos, sin; const defaultGraphColors = [ Colors.green, Colors.blue, Colors.red, Colors.orange, ]; class RadarChart extends StatefulWidget { final List ticks; final List features; final List> data; final bool reverseAxis; final TextStyle ticksTextStyle; final TextStyle featuresTextStyle; final Color outlineColor; final Color axisColor; final List graphColors; final int sides; const RadarChart({ Key? key, required this.ticks, required this.features, required this.data, this.reverseAxis = false, this.ticksTextStyle = const TextStyle(color: Colors.grey, fontSize: 12), this.featuresTextStyle = const TextStyle(color: Colors.black, fontSize: 16), this.outlineColor = Colors.black, this.axisColor = Colors.grey, this.graphColors = defaultGraphColors, this.sides = 0, }) : super(key: key); factory RadarChart.light({ required List ticks, required List features, required List> data, bool reverseAxis = false, bool useSides = false, }) { return RadarChart(ticks: ticks, features: features, data: data, reverseAxis: reverseAxis, sides: useSides ? features.length : 0); } factory RadarChart.dark({ required List ticks, required List features, required List> data, bool reverseAxis = false, bool useSides = false, }) { return RadarChart( ticks: ticks, features: features, data: data, featuresTextStyle: const TextStyle(color: Colors.white, fontSize: 16), outlineColor: Colors.white, axisColor: Colors.grey, reverseAxis: reverseAxis, sides: useSides ? features.length : 0); } @override _RadarChartState createState() => _RadarChartState(); } class _RadarChartState extends State with SingleTickerProviderStateMixin { double fraction = 0; late Animation animation; late AnimationController animationController; @override void initState() { super.initState(); animationController = AnimationController(duration: Duration(milliseconds: 1000), vsync: this); animation = Tween(begin: 0.0, end: 1.0).animate(CurvedAnimation( curve: Curves.fastOutSlowIn, parent: animationController, )) ..addListener(() { setState(() { fraction = animation.value; }); }); animationController.forward(); } @override void didUpdateWidget(RadarChart oldWidget) { super.didUpdateWidget(oldWidget); animationController.reset(); animationController.forward(); } @override Widget build(BuildContext context) { return CustomPaint( size: Size(double.infinity, double.infinity), painter: RadarChartPainter(widget.ticks, widget.features, widget.data, widget.reverseAxis, widget.ticksTextStyle, widget.featuresTextStyle, widget.outlineColor, widget.axisColor, widget.graphColors, widget.sides, this.fraction), ); } @override void dispose() { animationController.dispose(); super.dispose(); } } class RadarChartPainter extends CustomPainter { final List ticks; final List features; final List> data; final bool reverseAxis; final TextStyle ticksTextStyle; final TextStyle featuresTextStyle; final Color outlineColor; final Color axisColor; final List graphColors; final int sides; final double fraction; RadarChartPainter( this.ticks, this.features, this.data, this.reverseAxis, this.ticksTextStyle, this.featuresTextStyle, this.outlineColor, this.axisColor, this.graphColors, this.sides, this.fraction, ); Path variablePath(Size size, double radius, int sides) { var path = Path(); var angle = (math.pi * 2) / sides; Offset center = Offset(size.width / 2, size.height / 2); if (sides < 3) { // Draw a circle path.addOval(Rect.fromCircle( center: Offset(size.width / 2, size.height / 2), radius: radius, )); } else { // Draw a polygon Offset startPoint = Offset(radius * cos(-pi / 2), radius * sin(-pi / 2)); path.moveTo(startPoint.dx + center.dx, startPoint.dy + center.dy); for (int i = 1; i <= sides; i++) { double x = radius * cos(angle * i - pi / 2) + center.dx; double y = radius * sin(angle * i - pi / 2) + center.dy; path.lineTo(x, y); } path.close(); } return path; } @override void paint(Canvas canvas, Size size) { final centerX = size.width / 2.0; final centerY = size.height / 2.0; final centerOffset = Offset(centerX, centerY); final radius = math.min(centerX, centerY) * 0.8; final scale = radius / ticks.last; // Painting the chart outline var outlinePaint = Paint() ..color = outlineColor ..style = PaintingStyle.stroke ..strokeWidth = 2.0 ..isAntiAlias = true; var ticksPaint = Paint() ..color = axisColor ..style = PaintingStyle.stroke ..strokeWidth = 1.0 ..isAntiAlias = true; canvas.drawPath(variablePath(size, radius, this.sides), outlinePaint); // Painting the circles and labels for the given ticks (could be auto-generated) // The last tick is ignored, since it overlaps with the feature label var tickDistance = radius / (ticks.length); var tickLabels = reverseAxis ? ticks.reversed.toList() : ticks; if (reverseAxis) { TextPainter( text: TextSpan(text: tickLabels[0].toString(), style: ticksTextStyle), textDirection: TextDirection.ltr, ) ..layout(minWidth: 0, maxWidth: size.width) ..paint(canvas, Offset(centerX, centerY - ticksTextStyle.fontSize!)); } tickLabels.sublist(reverseAxis ? 1 : 0, reverseAxis ? ticks.length : ticks.length - 1).asMap().forEach((index, tick) { var tickRadius = tickDistance * (index + 1); canvas.drawPath(variablePath(size, tickRadius, this.sides), ticksPaint); TextPainter( text: TextSpan(text: tick.toString(), style: ticksTextStyle), textDirection: TextDirection.ltr, ) ..layout(minWidth: 0, maxWidth: size.width) ..paint(canvas, Offset(centerX, centerY - tickRadius - ticksTextStyle.fontSize!)); }); // Painting the axis for each given feature var angle = (2 * pi) / features.length; features.asMap().forEach((index, feature) { var xAngle = cos(angle * index - pi / 2); var yAngle = sin(angle * index - pi / 2); var featureOffset = Offset(centerX + radius * xAngle, centerY + radius * yAngle); canvas.drawLine(centerOffset, featureOffset, ticksPaint); var featureLabelFontHeight = featuresTextStyle.fontSize; var featureLabelFontWidth = featuresTextStyle.fontSize! - 5; var labelYOffset = yAngle < 0 ? -featureLabelFontHeight! : 0; var labelXOffset = xAngle < 0 ? -featureLabelFontWidth * feature.length : 0; TextPainter( text: TextSpan(text: feature, style: featuresTextStyle), textAlign: TextAlign.left, textDirection: TextDirection.ltr, ) ..layout(minWidth: 0, maxWidth: size.width) ..paint(canvas, Offset(featureOffset.dx + labelXOffset, featureOffset.dy + labelYOffset)); }); // Painting each graph data.asMap().forEach((index, graph) { var graphPaint = Paint() ..color = graphColors[index % graphColors.length].withOpacity(0.3) ..style = PaintingStyle.fill; var graphOutlinePaint = Paint() ..color = graphColors[index % graphColors.length] ..style = PaintingStyle.stroke ..strokeWidth = 2.0 ..isAntiAlias = true; // Start the graph on the initial point var scaledPoint = scale * graph[0] * fraction; var path = Path(); if (reverseAxis) { path.moveTo(centerX, centerY - (radius * fraction - scaledPoint)); } else { path.moveTo(centerX, centerY - scaledPoint); } graph.asMap().forEach((index, point) { if (index == 0) return; var xAngle = cos(angle * index - pi / 2); var yAngle = sin(angle * index - pi / 2); var scaledPoint = scale * point * fraction; if (reverseAxis) { path.lineTo(centerX + (radius * fraction - scaledPoint) * xAngle, centerY + (radius * fraction - scaledPoint) * yAngle); } else { path.lineTo(centerX + scaledPoint * xAngle, centerY + scaledPoint * yAngle); } }); path.close(); canvas.drawPath(path, graphPaint); canvas.drawPath(path, graphOutlinePaint); }); } @override bool shouldRepaint(RadarChartPainter oldDelegate) { return oldDelegate.fraction != fraction; } }