|
@@ -0,0 +1,282 @@
|
|
|
+import 'dart:ui';
|
|
|
+
|
|
|
+import 'package:flutter/material.dart';
|
|
|
+import 'dart:math' as math;
|
|
|
+
|
|
|
+class DimensionInfo {
|
|
|
+ final int maxValue;
|
|
|
+ final String title;
|
|
|
+
|
|
|
+ DimensionInfo({
|
|
|
+ required this.maxValue,
|
|
|
+ required this.title,
|
|
|
+ });
|
|
|
+}
|
|
|
+
|
|
|
+class RadarChart extends LeafRenderObjectWidget {
|
|
|
+ const RadarChart({Key? key}) : super(key: key);
|
|
|
+
|
|
|
+ @override
|
|
|
+ RenderObject createRenderObject(BuildContext context) {
|
|
|
+ return _RenderRadarChart();
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+class _RenderRadarChart extends RenderBox {
|
|
|
+ @override
|
|
|
+ void performLayout() {
|
|
|
+ size = constraints.constrain(Size.infinite);
|
|
|
+ }
|
|
|
+
|
|
|
+ @override
|
|
|
+ Size computeDryLayout(BoxConstraints constraints) {
|
|
|
+ return constraints.constrain(Size.infinite);
|
|
|
+ }
|
|
|
+
|
|
|
+ @override
|
|
|
+ void paint(PaintingContext context, Offset offset) {
|
|
|
+ super.paint(context, offset);
|
|
|
+ var canvas = context.canvas;
|
|
|
+ ChartPainter painter = RadarPainter();
|
|
|
+ painter._size = size;
|
|
|
+ canvas.save();
|
|
|
+ canvas.translate(offset.dx, offset.dy);
|
|
|
+ int count = canvas.getSaveCount();
|
|
|
+ painter.paintBackground(canvas);
|
|
|
+ painter.paintExtraInfo(canvas);
|
|
|
+ painter.paintDataInfo(canvas);
|
|
|
+ painter.paintForeground(canvas);
|
|
|
+ assert(count == canvas.getSaveCount(),
|
|
|
+ "Canvas save() or saveLayer() call times isn't equals restore()");
|
|
|
+ canvas.restore();
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+abstract class ChartPainter {
|
|
|
+ late Size _size;
|
|
|
+
|
|
|
+ Size get size => _size;
|
|
|
+
|
|
|
+ void paintBackground(Canvas canvas);
|
|
|
+
|
|
|
+ void paintExtraInfo(Canvas canvas);
|
|
|
+
|
|
|
+ void paintDataInfo(Canvas canvas);
|
|
|
+
|
|
|
+ void paintForeground(Canvas canvas);
|
|
|
+
|
|
|
+ Path dashPath(Path path) {
|
|
|
+ final PathMetrics pms = path.computeMetrics();
|
|
|
+ const double partLength = 4;
|
|
|
+
|
|
|
+ final newPath = Path();
|
|
|
+
|
|
|
+ for (var pm in pms) {
|
|
|
+ final int count = pm.length ~/ partLength;
|
|
|
+ for (int i = 0; i < count; i += 2) {
|
|
|
+ newPath.addPath(
|
|
|
+ pm.extractPath(partLength * i, partLength * (i + 1)), Offset.zero);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return newPath;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+class RadarPainter extends ChartPainter {
|
|
|
+ int maxDimension = 3;
|
|
|
+
|
|
|
+ double circle = 2 * math.pi;
|
|
|
+
|
|
|
+ double get maxRadius => size.shortestSide / 2;
|
|
|
+
|
|
|
+ List<String> titles = ["早期", "中期", "后期"];
|
|
|
+
|
|
|
+ // List<String> title2s = ["早期一二三", "中期四五六", "后期七八九", "晚期三六九", "初期二五八"];
|
|
|
+ List<String> title2s = ["早期一二三", "中期四五六", "后期七八九", "晚期"];
|
|
|
+ // List<String> title2s = ["早期一", "中期四五", "后期七八九"];
|
|
|
+ List<List<int>> data = [
|
|
|
+ [0, 0, 0, 0],
|
|
|
+ [0, 0, 0, 0],
|
|
|
+ [0, 0, 0, 0],
|
|
|
+ [0, 0, 1, 3],
|
|
|
+ [3, 1, 1, 1],
|
|
|
+ ];
|
|
|
+
|
|
|
+ List<Color> colors = [
|
|
|
+ Colors.orange,
|
|
|
+ Colors.indigo,
|
|
|
+ Colors.yellowAccent,
|
|
|
+ Colors.deepPurpleAccent,
|
|
|
+ Colors.pink,
|
|
|
+ ];
|
|
|
+
|
|
|
+ @override
|
|
|
+ void paintBackground(Canvas canvas) {
|
|
|
+ canvas.save();
|
|
|
+ canvas.translate(size.width / 2, size.height / 2);
|
|
|
+ final paint = Paint()
|
|
|
+ ..color = Colors.white
|
|
|
+ ..style = PaintingStyle.stroke
|
|
|
+ ..strokeWidth = 0.5;
|
|
|
+
|
|
|
+ for (int i = 0; i < maxDimension; i++) {
|
|
|
+ var radius = maxRadius / maxDimension * i;
|
|
|
+ var path = Path()
|
|
|
+ ..addOval(Rect.fromCircle(center: Offset.zero, radius: radius));
|
|
|
+ canvas.drawPath(dashPath(path), paint);
|
|
|
+ // canvas.drawCircle(Offset.zero, radius, paint);
|
|
|
+ }
|
|
|
+
|
|
|
+ canvas.drawCircle(Offset.zero, maxRadius, paint..strokeWidth = 1);
|
|
|
+ canvas.restore();
|
|
|
+ }
|
|
|
+
|
|
|
+ @override
|
|
|
+ void paintDataInfo(Canvas canvas) {
|
|
|
+ canvas.save();
|
|
|
+ canvas.translate(size.width / 2, size.height / 2);
|
|
|
+ double strokeWidth = 0;
|
|
|
+
|
|
|
+ for (int i = 0; i < data.length; i++) {
|
|
|
+ var path = Path();
|
|
|
+ strokeWidth = 2 * (i + 1);
|
|
|
+ for (int j = 0; j < data[i].length; j++) {
|
|
|
+ var radius = circle / data[i].length * (j) - math.pi / 2;
|
|
|
+
|
|
|
+ if (j == 0) {
|
|
|
+ path.moveTo(
|
|
|
+ data[i][j] == 0
|
|
|
+ ? strokeWidth * math.cos(radius)
|
|
|
+ : maxRadius * data[i][j] / maxDimension * math.cos(radius),
|
|
|
+ data[i][j] == 0
|
|
|
+ ? strokeWidth * math.sin(radius)
|
|
|
+ : maxRadius * data[i][j] / maxDimension * math.sin(radius),
|
|
|
+ );
|
|
|
+ } else {
|
|
|
+ path.lineTo(
|
|
|
+ data[i][j] == 0
|
|
|
+ ? strokeWidth * math.cos(radius)
|
|
|
+ : maxRadius * data[i][j] / maxDimension * math.cos(radius),
|
|
|
+ data[i][j] == 0
|
|
|
+ ? strokeWidth * math.sin(radius)
|
|
|
+ : maxRadius * data[i][j] / maxDimension * math.sin(radius),
|
|
|
+ );
|
|
|
+ }
|
|
|
+ }
|
|
|
+ path.close();
|
|
|
+ canvas.drawPath(
|
|
|
+ path,
|
|
|
+ Paint()
|
|
|
+ ..color = colors[i].withOpacity(0.2 + 0.8 * ((i + 1) / data.length))
|
|
|
+ ..style = PaintingStyle.stroke
|
|
|
+ ..strokeWidth = 2);
|
|
|
+ }
|
|
|
+
|
|
|
+ canvas.restore();
|
|
|
+ }
|
|
|
+
|
|
|
+ @override
|
|
|
+ void paintExtraInfo(Canvas canvas) {
|
|
|
+ canvas.save();
|
|
|
+ canvas.translate(size.width / 2, size.height / 2);
|
|
|
+
|
|
|
+ for (int i = 0; i < maxDimension; i++) {
|
|
|
+ canvas.save();
|
|
|
+
|
|
|
+ var radius = maxRadius / maxDimension * (i + 1);
|
|
|
+
|
|
|
+ canvas.translate(
|
|
|
+ radius * math.cos(math.pi / 4), radius * math.sin(math.pi / 4));
|
|
|
+ canvas.drawCircle(Offset.zero, 4, Paint()..color = Colors.red);
|
|
|
+
|
|
|
+ TextPainter tp = TextPainter();
|
|
|
+ tp.textDirection = TextDirection.ltr;
|
|
|
+ tp.text = TextSpan(
|
|
|
+ text: titles[i],
|
|
|
+ style: const TextStyle(
|
|
|
+ color: Color(0xFFB9BEE6),
|
|
|
+ fontSize: 7,
|
|
|
+ height: 10 / 7,
|
|
|
+ backgroundColor: Colors.green),
|
|
|
+ );
|
|
|
+ tp.layout(maxWidth: double.infinity);
|
|
|
+
|
|
|
+ var tpSize = tp.size;
|
|
|
+
|
|
|
+ canvas.translate(-tpSize.width / 2, -tpSize.height / 2);
|
|
|
+
|
|
|
+ // canvas.drawRect(
|
|
|
+ // Rect.fromCenter(
|
|
|
+ // center: Offset.zero,
|
|
|
+ // width: tpSize.width + 15,
|
|
|
+ // height: tpSize.height + 15,
|
|
|
+ // ),
|
|
|
+ // Paint()..color = Colors.green,
|
|
|
+ // );
|
|
|
+
|
|
|
+ tp.paint(canvas, Offset.zero);
|
|
|
+
|
|
|
+ canvas.restore();
|
|
|
+ }
|
|
|
+ canvas.restore();
|
|
|
+
|
|
|
+ canvas.save();
|
|
|
+ canvas.translate(size.width / 2, size.height / 2);
|
|
|
+
|
|
|
+ for (int i = 0; i < title2s.length; i++) {
|
|
|
+ canvas.save();
|
|
|
+
|
|
|
+ /// 计算文字大小
|
|
|
+ TextPainter tp = TextPainter();
|
|
|
+ tp.textDirection = TextDirection.ltr;
|
|
|
+ tp.text = TextSpan(
|
|
|
+ text: title2s[i],
|
|
|
+ style: const TextStyle(
|
|
|
+ color: Color(0xFFB9BEE6),
|
|
|
+ fontSize: 7,
|
|
|
+ height: 10 / 7,
|
|
|
+ backgroundColor: Colors.red),
|
|
|
+ );
|
|
|
+ tp.layout(maxWidth: double.infinity);
|
|
|
+
|
|
|
+ /// 旋转角度
|
|
|
+ var radius = circle / title2s.length * i - math.pi / 2;
|
|
|
+
|
|
|
+ canvas.drawLine(
|
|
|
+ Offset.zero,
|
|
|
+ Offset(maxRadius * math.cos(radius), maxRadius * math.sin(radius)),
|
|
|
+ Paint()
|
|
|
+ ..color = Colors.yellow
|
|
|
+ ..strokeWidth = 1,
|
|
|
+ );
|
|
|
+
|
|
|
+ double textRadius = math.sqrt(
|
|
|
+ math.pow(tp.width / 2 * math.cos(radius), 2) +
|
|
|
+ math.pow(tp.height / 2 * math.sin(radius), 2),
|
|
|
+ );
|
|
|
+
|
|
|
+ canvas.translate(
|
|
|
+ (maxRadius + 10 + textRadius) * math.cos(radius),
|
|
|
+ (maxRadius + 10 + textRadius) * math.sin(radius),
|
|
|
+ );
|
|
|
+
|
|
|
+ // canvas.drawCircle(Offset.zero, 14, Paint()..color = Colors.blue);
|
|
|
+
|
|
|
+ var tpSize = tp.size;
|
|
|
+
|
|
|
+ canvas.translate(
|
|
|
+ -tpSize.width / 2,
|
|
|
+ -tpSize.height / 2,
|
|
|
+ );
|
|
|
+ //
|
|
|
+ tp.paint(canvas, Offset.zero);
|
|
|
+ canvas.restore();
|
|
|
+ }
|
|
|
+ canvas.restore();
|
|
|
+ }
|
|
|
+
|
|
|
+ @override
|
|
|
+ void paintForeground(Canvas canvas) {
|
|
|
+ // TODO: implement paintForeground
|
|
|
+ }
|
|
|
+}
|