728x90
레이더 차트의 구성
레이더 차트를 만들기 위해서 많은 요소가 필요하지만 그 중에서 레이더 차트를 구성하는 기본 요소들이 있다.
- ticks: 레이더 차트의 눈금값(여기서 빨간색 글자로 20, 40, 60...으로 표시된 것들)
- features: 레이더 차트의 각 축에 해당되는 라벨(여기서 파란색 글자로 창의력, 실행력...으로 표시된 것들)
- data: 실제 데이터 값. 이 값들을 기준으로 그래프가 그려지게 됨(검정색 글자로 50, 60, 70...으로 표시된 것들)
- sides: 다각형의 변의 개수(여기서는 육각형이므로 sides = 6이다.)
주의할 점
만약 sides의 값이 features나 data값의 개수보다 작거나 크면 차트가 제대로 생성되지 않는 오류가 발생할 수 있다.
이러한 오류들을 방지해주기 위해서는 features나 data 값의 개수에 맞게 sides의 값을 맞춰줘야 한다.
- sides > (features or data)
- sides를 8로 한 경우 8개의 변이 생기면서 그래프가 제대로 그려지지 않게되며 터미널 상에서 오류 발생
- sides < (features or data)
- sides를 3으로 한 경우 3개의 변을 가진 다각형이 생성되면서 그래프도 그려지고 오류도 발생되지 않지만 features와 data의 값이 겹치게 되면서 의도한 그래프가 그려지지 않게 됨.
코드
import 'dart:math' as math;
import 'package:flutter/material.dart';
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<num>> 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({
super.key,
this.ticks = const [20, 40, 60, 80, 90, 100], // 기본 눈금 값
required this.features, // 필수로 받아야 하는 레이블들
required this.data, // 필수로 받아야 하는 데이터들
this.reverseAxis = false, // 반전 축 사용 여부 (사용하지 않음)
this.ticksTextStyle = const TextStyle(color: Colors.red, fontSize: 12),
this.featuresTextStyle = const TextStyle(color: Colors.blue, fontSize: 16),
this.outlineColor = const Color.fromARGB(255, 50, 17, 17),
this.axisColor = Colors.green, // 축 색상
required this.sides, // 기본 다각형 변 개수
this.graphColors = defaultGraphColors, // 기본 그래프 색상
});
@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();
// 애니메이션 컨트롤러 생성, 1초 동안 애니메이션 진행
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(
// CustomPainter를 이용해 레이더 차트 그리기
size: Size(180, 180), // 화면 전체 크기
painter: RadarChartPainter(
widget.ticks, // 눈금 값 전달
widget.features, // 축 레이블 전달
widget.data, // 데이터 전달
widget.reverseAxis, // 축 반전 여부 (사용하지 않음)
widget.ticksTextStyle, // 눈금 텍스트 스타일 전달
widget.featuresTextStyle, // 축 레이블 텍스트 스타일 전달
widget.outlineColor, // 외곽선 색상 전달
widget.axisColor, // 축 색상 전달 (사용하지 않음)
widget.graphColors, // 그래프 색상 전달
widget.sides, // 다각형 변 개수 전달
fraction, // 애니메이션 진행 값 전달
),
);
}
@override
void dispose() {
// 애니메이션 컨트롤러 해제
animationController.dispose();
super.dispose();
}
}
//레이더 차트를 그리기 위한 class
class RadarChartPainter extends CustomPainter {
// 레이더 차트의 요소들을 정의하는 클래스
final List<int> ticks; // 눈금 값 리스트
final List<String> features; // 축 레이블
final List<List<num>> 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,
);
@override
void paint(Canvas canvas, Size size) {
final Paint outlinePaint = Paint()
..color = outlineColor // 외곽선 색상
..strokeWidth = 2.0 // 외곽선 두께
..style = PaintingStyle.stroke; // 외곽선 그리기 모드
final double radius = size.width / 2; // 차트의 반지름
final Offset center = Offset(size.width / 2, size.height / 2); // 차트 중심
// 외곽 다각형 그리기
final Path path = Path();
for (int i = 0; i < sides; i++) {
double angle = (2 * math.pi * i) / sides; // 각도를 계산
double x = center.dx + radius * fraction * math.cos(angle); // X 좌표
double y = center.dy + radius * fraction * math.sin(angle); // Y 좌표
if (i == 0) {
path.moveTo(x, y); // 첫 번째 점으로 이동
} else {
path.lineTo(x, y); // 나머지 점들을 연결
}
}
path.close(); // 마지막 점과 첫 번째 점 연결
canvas.drawPath(path, outlinePaint); // 외곽선 그리기
// Ticks 그리기
for (int i = 0; i < ticks.length; i++) {
double tickRadius = (radius / ticks.length) * (i + 1);
Path tickPath = Path();
for (int j = 0; j < sides; j++) {
double angle = (2 * math.pi * j) / sides;
double x = center.dx + tickRadius * math.cos(angle);
double y = center.dy + tickRadius * math.sin(angle);
if (j == 0) {
tickPath.moveTo(x, y);
} else {
tickPath.lineTo(x, y);
}
}
tickPath.close();
canvas.drawPath(tickPath, outlinePaint);
// 눈금 텍스트 표시
final textPainter = TextPainter(
text: TextSpan(text: '${ticks[i]}', style: ticksTextStyle),
textDirection: TextDirection.ltr,
);
textPainter.layout();
textPainter.paint(
canvas,
Offset(center.dx - textPainter.width - 5, center.dy - tickRadius),
);
}
// Features 텍스트 그리기
for (int i = 0; i < features.length; i++) {
double angle = (2 * math.pi * i) / sides;
double x = center.dx + (radius + 20) * math.cos(angle); // 축 끝점
double y = center.dy + (radius + 20) * math.sin(angle); // 축 끝점
final textPainter = TextPainter(
text: TextSpan(text: features[i], style: featuresTextStyle),
textDirection: TextDirection.ltr,
);
textPainter.layout();
// 축 끝점에 텍스트 그리기
textPainter.paint(
canvas,
Offset(x - textPainter.width / 2, y - textPainter.height / 2),
);
}
// 각 데이터 셋에 따른 레이더 차트 그리기
for (int i = 0; i < data.length; i++) {
final List<num> dataset = data[i];
final Paint dataPaint = Paint()
..color =
graphColors[i % graphColors.length].withOpacity(0.8) // 데이터셋 색상
..style = PaintingStyle.fill; // 내부를 채우는 스타일
final Path dataPath = Path();
for (int j = 0; j < sides; j++) {
double angle = (2 * math.pi * j) / sides; // 각도 계산
double value = dataset[j] / ticks.last; // 비율 계산 (데이터 값 / 최대 눈금 값)
double x =
center.dx + radius * value * fraction * math.cos(angle); // X 좌표
double y =
center.dy + radius * value * fraction * math.sin(angle); // Y 좌표
if (j == 0) {
dataPath.moveTo(x, y); // 첫 번째 데이터 점
} else {
dataPath.lineTo(x, y); // 다른 데이터 점들 연결
}
//데이터 값을 텍스트로 표시
final dataTextPainter = TextPainter(
text: TextSpan(
text: '${dataset[j]}',
style: TextStyle(color: Colors.black, fontSize: 12),
),
textDirection: TextDirection.ltr,
);
dataTextPainter.layout();
dataTextPainter.paint(
canvas,
Offset(x - dataTextPainter.width / 2, y - dataTextPainter.height / 2),
);
if (j == 0) {
dataPath.moveTo(x, y);
} else {
dataPath.lineTo(x, y);
}
}
dataPath.close(); // 데이터 경로 닫기
canvas.drawPath(dataPath, dataPaint); // 데이터 차트 그리기
}
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) {
return true; // 애니메이션이 진행될 때마다 다시 그려야 하므로 true
}
}
'앱개발 > flutter' 카테고리의 다른 글
[Flutter] MVVM 아키텍처(Model-View-ViewModel) (0) | 2025.03.23 |
---|---|
[Flutter] JSON, jsonEncode(toJson), jsonDecode(fromJson) (2) | 2025.03.21 |
[Flutter] TextField, TextFormField, TextEditingController (0) | 2025.03.20 |
[Flutter] 레이아웃 관련 위젯(2) : Row, Column, Stack (2) | 2025.03.19 |
[Flutter] 레이아웃 관련 위젯(1) : Container, SizedBox, Card (0) | 2025.03.18 |