297 lines
8.7 KiB
Dart
297 lines
8.7 KiB
Dart
library flutter_radar_chart;
|
|
|
|
import 'dart:ui';
|
|
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<int> ticks;
|
|
final List<String> features;
|
|
final List<List<int>> data;
|
|
final bool reverseAxis;
|
|
final TextStyle ticksTextStyle;
|
|
final TextStyle featuresTextStyle;
|
|
final Color outlineColor;
|
|
final Color axisColor;
|
|
final List<Color> 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<int> ticks,
|
|
required List<String> features,
|
|
required List<List<int>> 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<int> ticks,
|
|
required List<String> features,
|
|
required List<List<int>> 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<RadarChart> with SingleTickerProviderStateMixin {
|
|
double fraction = 0;
|
|
late Animation<double> 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<int> ticks;
|
|
final List<String> features;
|
|
final List<List<int>> data;
|
|
final bool reverseAxis;
|
|
final TextStyle ticksTextStyle;
|
|
final TextStyle featuresTextStyle;
|
|
final Color outlineColor;
|
|
final Color axisColor;
|
|
final List<Color> 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;
|
|
}
|
|
}
|