swing_account/lib/widgets/statistics/monthly_chart.dart
2025-01-02 13:39:25 +08:00

244 lines
6.2 KiB
Dart
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import 'package:flutter/material.dart';
import '../../models/record.dart';
class MonthlyChart extends StatelessWidget {
final List<Record> records;
const MonthlyChart({
Key? key,
required this.records,
}) : super(key: key);
@override
Widget build(BuildContext context) {
// 按月份分组数据
final monthlyData = _getMonthlyData();
if (monthlyData.isEmpty) return const SizedBox();
// 计算最大值用于缩放
final maxAmount = monthlyData.fold(0.0, (max, data) {
final monthMax = data.income > data.expense ? data.income : data.expense;
return monthMax > max ? monthMax : max;
});
return Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 8,
offset: const Offset(0, 2),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'每月收支',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 20),
SizedBox(
height: 200,
child: CustomPaint(
size: const Size(double.infinity, 200),
painter: ChartPainter(
monthlyData: monthlyData,
maxAmount: maxAmount,
),
),
),
const SizedBox(height: 8),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
_buildLegend('支出', Colors.red),
const SizedBox(width: 24),
_buildLegend('收入', Colors.green),
],
),
],
),
);
}
List<MonthData> _getMonthlyData() {
final Map<int, MonthData> monthlyMap = {};
// 初始化12个月的数据
for (int month = 1; month <= 12; month++) {
monthlyMap[month] = MonthData(month: month);
}
// 填充实际数据
for (final record in records) {
final month = record.createTime.month;
if (record.type == RecordType.expense) {
monthlyMap[month]!.expense += record.amount;
} else {
monthlyMap[month]!.income += record.amount;
}
}
return monthlyMap.values.toList();
}
Widget _buildLegend(String label, Color color) {
return Row(
children: [
Container(
width: 12,
height: 12,
decoration: BoxDecoration(
color: color,
shape: BoxShape.circle,
),
),
const SizedBox(width: 4),
Text(label),
],
);
}
}
class MonthData {
final int month;
double expense;
double income;
MonthData({
required this.month,
this.expense = 0,
this.income = 0,
});
}
class ChartPainter extends CustomPainter {
final List<MonthData> monthlyData;
final double maxAmount;
ChartPainter({
required this.monthlyData,
required this.maxAmount,
});
@override
void paint(Canvas canvas, Size size) {
final expensePath = Path();
final incomePath = Path();
final expensePaint = Paint()
..color = Colors.red.withOpacity(0.8)
..style = PaintingStyle.stroke
..strokeWidth = 2;
final incomePaint = Paint()
..color = Colors.green.withOpacity(0.8)
..style = PaintingStyle.stroke
..strokeWidth = 2;
final width = size.width;
final height = size.height - 20; // 留出底部空间显示月份
final monthWidth = width / 11; // 12个月11个间隔
// 绘制网格线
final gridPaint = Paint()
..color = Colors.grey.withOpacity(0.2)
..style = PaintingStyle.stroke
..strokeWidth = 1;
// 横向网格线
for (var i = 0; i <= 4; i++) {
final y = height * i / 4;
canvas.drawLine(
Offset(0, y),
Offset(width, y),
gridPaint,
);
}
// 添加金额标签
void drawAmountLabel(double x, double y, double amount) {
if (amount == 0) return;
final textPainter = TextPainter(
text: TextSpan(
text: amount >= 1000
? '${(amount / 1000).toStringAsFixed(1)}k'
: amount.toStringAsFixed(0),
style: TextStyle(
color: Colors.grey[600],
fontSize: 10,
),
),
textDirection: TextDirection.ltr,
);
textPainter.layout();
textPainter.paint(
canvas,
Offset(x - textPainter.width / 2, y - 15),
);
}
// 绘制折线和数据点
for (var i = 0; i < monthlyData.length; i++) {
final x = i * monthWidth;
final expenseY = height - (height * monthlyData[i].expense / maxAmount);
final incomeY = height - (height * monthlyData[i].income / maxAmount);
if (i == 0) {
expensePath.moveTo(x, expenseY);
incomePath.moveTo(x, incomeY);
} else {
expensePath.lineTo(x, expenseY);
incomePath.lineTo(x, incomeY);
}
// 绘制数据点和金额标签
if (monthlyData[i].expense > 0) {
canvas.drawCircle(
Offset(x, expenseY),
3,
Paint()..color = Colors.red,
);
drawAmountLabel(x, expenseY, monthlyData[i].expense);
}
if (monthlyData[i].income > 0) {
canvas.drawCircle(
Offset(x, incomeY),
3,
Paint()..color = Colors.green,
);
drawAmountLabel(x, incomeY, monthlyData[i].income);
}
// 绘制月份
final textPainter = TextPainter(
text: TextSpan(
text: '${monthlyData[i].month}',
style: TextStyle(
color: Colors.grey[600],
fontSize: 10,
),
),
textDirection: TextDirection.ltr,
);
textPainter.layout();
textPainter.paint(
canvas,
Offset(x - textPainter.width / 2, height + 5),
);
}
canvas.drawPath(expensePath, expensePaint);
canvas.drawPath(incomePath, incomePaint);
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) => true;
}