feat:增加统计图表模块
This commit is contained in:
@@ -86,28 +86,62 @@ class AppDrawer extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildVersionInfo({
|
||||
required BuildContext context,
|
||||
required String fullVersion,
|
||||
}) {
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
padding: EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
border: Border(top: BorderSide(color: Colors.grey.shade300)),
|
||||
),
|
||||
child: Text(
|
||||
'版本 v$fullVersion',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final appProvider = Provider.of<AppProvider>(context);
|
||||
|
||||
return Drawer(
|
||||
child: ListView(
|
||||
padding: EdgeInsets.zero,
|
||||
child: Column(
|
||||
children: [
|
||||
// 抽屉头部
|
||||
buildDrawerHeader(context),
|
||||
// 主要内容区域,可以滚动
|
||||
Expanded(
|
||||
child: ListView(
|
||||
padding: EdgeInsets.zero,
|
||||
children: [
|
||||
// 抽屉头部
|
||||
buildDrawerHeader(context),
|
||||
_buildThemeSection(context, appProvider),
|
||||
const Divider(),
|
||||
_buildDrawerItem(
|
||||
context: context,
|
||||
icon: Icons.help,
|
||||
title: '帮助与反馈',
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
// 这里可以导航到帮助页面
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
_buildThemeSection(context, appProvider),
|
||||
|
||||
const Divider(),
|
||||
|
||||
_buildDrawerItem(
|
||||
context: context,
|
||||
icon: Icons.help,
|
||||
title: '帮助与反馈',
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
// 这里可以导航到帮助页面
|
||||
Consumer<AppProvider>(
|
||||
builder: (context, appProvider, child) {
|
||||
return buildVersionInfo(
|
||||
context: context,
|
||||
fullVersion: appProvider.fullVersion,
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'package:flisp_app/layout/app_drawer.dart';
|
||||
import 'package:flisp_app/pages/calendar_page.dart';
|
||||
import 'package:flisp_app/pages/flisp_page.dart';
|
||||
import 'package:flisp_app/pages/stats_page.dart';
|
||||
import 'package:flisp_app/pages/todo_page.dart';
|
||||
import 'package:flisp_app/provider/app_provider.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
@@ -25,6 +26,7 @@ class _MainScreenState extends State<MainScreen> {
|
||||
icon: Icon(Icons.calendar_month_rounded),
|
||||
label: '日程',
|
||||
),
|
||||
BottomNavigationBarItem(icon: Icon(Icons.insert_chart), label: '统计'),
|
||||
];
|
||||
|
||||
@override
|
||||
@@ -89,7 +91,9 @@ class _MainScreenState extends State<MainScreen> {
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildFloatingButton(BuildContext context, int index) {
|
||||
Widget? _buildFloatingButton(BuildContext context, int index) {
|
||||
if (index == 3) return null;
|
||||
|
||||
return FloatingActionButton(
|
||||
onPressed: () => _onPressFloatingButton(index),
|
||||
backgroundColor: Colors.transparent,
|
||||
@@ -115,6 +119,8 @@ class _MainScreenState extends State<MainScreen> {
|
||||
return TodoPage(key: _todoPageKey);
|
||||
case 2:
|
||||
return CalendarPage(key: _calendarPageKey);
|
||||
case 3:
|
||||
return StatsPage();
|
||||
default:
|
||||
return FlispPage(key: _flispPageKey);
|
||||
}
|
||||
|
||||
@@ -54,8 +54,8 @@ class CalendarPageState extends State<CalendarPage> {
|
||||
void _showDialog(bool isEditing, Calendar? calendar) {
|
||||
final provider = Provider.of<CalendarProvider>(context, listen: false);
|
||||
|
||||
if (isEditing) {
|
||||
provider.initForm(calendar!);
|
||||
if (calendar != null) {
|
||||
provider.initForm(calendar);
|
||||
} else {
|
||||
provider.resetForm();
|
||||
}
|
||||
@@ -153,7 +153,7 @@ class CalendarPageState extends State<CalendarPage> {
|
||||
dataSource: AppointmentDataSource(_appointments),
|
||||
onAdd: () {
|
||||
if (_calendarController.view == CalendarView.week) {
|
||||
_showDialog(true, provider.formItem);
|
||||
_showDialog(false, provider.formItem);
|
||||
}
|
||||
},
|
||||
onEdit: (appointment) {
|
||||
|
||||
209
lib/pages/stats_page.dart
Normal file
209
lib/pages/stats_page.dart
Normal file
@@ -0,0 +1,209 @@
|
||||
import 'package:flisp_app/models/flisp.dart';
|
||||
import 'package:flisp_app/models/todo.dart';
|
||||
import 'package:flisp_app/service/flisp_service.dart';
|
||||
import 'package:flisp_app/service/todo_service.dart';
|
||||
import 'package:flisp_app/widgets/chart.dart';
|
||||
import 'package:flisp_app/widgets/common.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class StatsPage extends StatefulWidget {
|
||||
const StatsPage({super.key});
|
||||
|
||||
@override
|
||||
StatsPageState createState() => StatsPageState();
|
||||
}
|
||||
|
||||
class StatsPageState extends State<StatsPage> {
|
||||
final double chartHeight = 400;
|
||||
final FlispService flispService = FlispService();
|
||||
final TodoService todoService = TodoService();
|
||||
|
||||
late List<ChartData> flispMonthlyStats;
|
||||
late List<ChartData> flispCategoryStats;
|
||||
late List<ChartData> todoMonthlyStats;
|
||||
late List<ChartData> todoMonthlyCompleteStats;
|
||||
late List<ChartData> todoPriorityStats;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
final allFlisps = flispService.getAllFlisps();
|
||||
getFlispMonthlyStats(allFlisps);
|
||||
getFlispCategoryStats(allFlisps);
|
||||
|
||||
final allTodos = todoService.getAllTodos();
|
||||
getTodoMonthlyStats(allTodos);
|
||||
getTodoMonthlyCompleteStats(allTodos);
|
||||
getTodoCategoryStats(allTodos);
|
||||
}
|
||||
|
||||
void getFlispMonthlyStats(List<Flisp> flisps) {
|
||||
Map<String, double> countMap = {};
|
||||
|
||||
for (var flisp in flisps) {
|
||||
String month = '${flisp.createTime.month}月';
|
||||
countMap[month] = (countMap[month] ?? 0) + 1;
|
||||
}
|
||||
|
||||
flispMonthlyStats = convert2ChartData(countMap);
|
||||
}
|
||||
|
||||
void getFlispCategoryStats(List<Flisp> flisps) {
|
||||
Map<String, double> countMap = {};
|
||||
|
||||
for (var flisp in flisps) {
|
||||
String category = flisp.tag.label;
|
||||
countMap[category] = (countMap[category] ?? 0) + 1;
|
||||
}
|
||||
|
||||
flispCategoryStats = convert2ChartData(countMap);
|
||||
}
|
||||
|
||||
void getTodoMonthlyStats(List<Todo> todos) {
|
||||
Map<String, double> countMap = {};
|
||||
|
||||
for (var todo in todos) {
|
||||
String month = '${todo.createTime.month}月';
|
||||
countMap[month] = (countMap[month] ?? 0) + 1;
|
||||
}
|
||||
|
||||
todoMonthlyStats = convert2ChartData(countMap);
|
||||
}
|
||||
|
||||
void getTodoMonthlyCompleteStats(List<Todo> todos) {
|
||||
Map<String, double> countMap = {};
|
||||
|
||||
for (var todo in todos) {
|
||||
if (todo.isCompleted) {
|
||||
String month = '${todo.createTime.month}月';
|
||||
countMap[month] = (countMap[month] ?? 0) + 1;
|
||||
}
|
||||
}
|
||||
|
||||
todoMonthlyCompleteStats = convert2ChartData(countMap);
|
||||
}
|
||||
|
||||
void getTodoCategoryStats(List<Todo> todos) {
|
||||
Map<String, double> countMap = {};
|
||||
|
||||
for (var todo in todos) {
|
||||
String priority = todo.priority.label;
|
||||
countMap[priority] = (countMap[priority] ?? 0) + 1;
|
||||
}
|
||||
|
||||
todoPriorityStats = convert2ChartData(countMap);
|
||||
}
|
||||
|
||||
List<ChartData> convert2ChartData(Map<String, double> countMap) {
|
||||
return countMap.entries
|
||||
.map((e) => ChartData(name: e.key, value: e.value))
|
||||
.toList();
|
||||
}
|
||||
|
||||
Widget _buildFlispMonthlyStats(BuildContext context) {
|
||||
return Column(
|
||||
children: [
|
||||
buildChartTitle(context, '每月闪灵数量统计'),
|
||||
const SizedBox(height: 3),
|
||||
buildChartDivider(context),
|
||||
const SizedBox(height: 3),
|
||||
Expanded(
|
||||
child: lineChart(
|
||||
context: context,
|
||||
xAxisName: '月份',
|
||||
yAxisName: '数量',
|
||||
unit: '篇',
|
||||
data: flispMonthlyStats,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildFlispCategoryStats(BuildContext context) {
|
||||
return Column(
|
||||
children: [
|
||||
buildChartTitle(context, '闪灵分类统计'),
|
||||
const SizedBox(height: 3),
|
||||
buildChartDivider(context),
|
||||
const SizedBox(height: 3),
|
||||
Expanded(
|
||||
child: pieChart(
|
||||
context: context,
|
||||
unit: '篇',
|
||||
data: flispCategoryStats,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTodoMonthlyStats(BuildContext context) {
|
||||
return Column(
|
||||
children: [
|
||||
buildChartTitle(context, '每月待办数量统计'),
|
||||
const SizedBox(height: 3),
|
||||
buildChartDivider(context),
|
||||
const SizedBox(height: 3),
|
||||
Expanded(
|
||||
child: doubleBarChart(
|
||||
context: context,
|
||||
xAxisName: '月份',
|
||||
yAxisName: '数量',
|
||||
unit: '个',
|
||||
data1: todoMonthlyStats,
|
||||
data2: todoMonthlyCompleteStats,
|
||||
series1Name: '总数',
|
||||
series2Name: '完成数',
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTodoPriorityStats(BuildContext context) {
|
||||
return Column(
|
||||
children: [
|
||||
buildChartTitle(context, '待办分类统计'),
|
||||
const SizedBox(height: 3),
|
||||
buildChartDivider(context),
|
||||
const SizedBox(height: 3),
|
||||
Expanded(
|
||||
child: pieChart(context: context, unit: '个', data: todoPriorityStats),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SingleChildScrollView(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(10),
|
||||
child: Column(
|
||||
children: [
|
||||
SizedBox(
|
||||
height: chartHeight,
|
||||
child: BuildCard(child: _buildFlispMonthlyStats(context)),
|
||||
),
|
||||
SizedBox(height: 10),
|
||||
SizedBox(
|
||||
height: chartHeight,
|
||||
child: BuildCard(child: _buildFlispCategoryStats(context)),
|
||||
),
|
||||
SizedBox(height: 10),
|
||||
SizedBox(
|
||||
height: chartHeight,
|
||||
child: BuildCard(child: _buildTodoMonthlyStats(context)),
|
||||
),
|
||||
SizedBox(height: 10),
|
||||
SizedBox(
|
||||
height: chartHeight,
|
||||
child: BuildCard(child: _buildTodoPriorityStats(context)),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'package:flisp_app/utils/theme_utils.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
class AppProvider with ChangeNotifier {
|
||||
@@ -9,6 +10,9 @@ class AppProvider with ChangeNotifier {
|
||||
int _currentTab = 0;
|
||||
bool _isDarkMode = false;
|
||||
ThemeColor _currentTheme = defaultThemes[0];
|
||||
String _appVersion = '1.0.0';
|
||||
String _buildNumber = '1';
|
||||
|
||||
late SharedPreferences _prefs;
|
||||
bool _isInitialized = false;
|
||||
|
||||
@@ -20,6 +24,8 @@ class AppProvider with ChangeNotifier {
|
||||
Future<void> _initPreferences() async {
|
||||
_prefs = await SharedPreferences.getInstance();
|
||||
_loadPreferences();
|
||||
await _getVersionInfo();
|
||||
|
||||
_isInitialized = true;
|
||||
notifyListeners();
|
||||
}
|
||||
@@ -31,12 +37,18 @@ class AppProvider with ChangeNotifier {
|
||||
// 加载主题色
|
||||
final themeName = _prefs.getString(_themeKey);
|
||||
if (themeName != null) {
|
||||
// 根据主题名称查找对应的主题
|
||||
final savedTheme = defaultThemes.firstWhere(
|
||||
(theme) => theme.name == themeName,
|
||||
orElse: () => defaultThemes[0],
|
||||
);
|
||||
_currentTheme = savedTheme;
|
||||
_currentTheme = getThemeColor(themeName);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _getVersionInfo() async {
|
||||
try {
|
||||
PackageInfo packageInfo = await PackageInfo.fromPlatform();
|
||||
_appVersion = packageInfo.version;
|
||||
_buildNumber = packageInfo.buildNumber;
|
||||
notifyListeners();
|
||||
} catch (e) {
|
||||
print('获取版本信息失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,6 +72,8 @@ class AppProvider with ChangeNotifier {
|
||||
|
||||
bool get isInitialized => _isInitialized;
|
||||
|
||||
String get fullVersion => '$_appVersion+$_buildNumber';
|
||||
|
||||
void changeTab(int index) {
|
||||
_currentTab = index;
|
||||
notifyListeners();
|
||||
@@ -90,4 +104,4 @@ class AppProvider with ChangeNotifier {
|
||||
fontFamily: 'CustomFont',
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -146,6 +146,13 @@ final List<ThemeColor> defaultThemes = [
|
||||
),
|
||||
];
|
||||
|
||||
ThemeColor getThemeColor(String themeName) {
|
||||
return defaultThemes.firstWhere(
|
||||
(theme) => theme.name == themeName,
|
||||
orElse: () => defaultThemes[0],
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildThemeColorList(BuildContext context, AppProvider appProvider) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
|
||||
|
||||
297
lib/widgets/chart.dart
Normal file
297
lib/widgets/chart.dart
Normal file
@@ -0,0 +1,297 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:syncfusion_flutter_charts/charts.dart';
|
||||
|
||||
class ChartData {
|
||||
final String name;
|
||||
final double value;
|
||||
|
||||
ChartData({required this.name, required this.value});
|
||||
}
|
||||
|
||||
Widget buildChartTitle(BuildContext context, String title) {
|
||||
return Row(
|
||||
children: [
|
||||
Icon(Icons.insert_chart, color: Theme.of(context).colorScheme.primary),
|
||||
Text(title),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildChartDivider(BuildContext context) {
|
||||
return Divider(
|
||||
height: 1,
|
||||
thickness: 1,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
indent: 0,
|
||||
endIndent: 0,
|
||||
);
|
||||
}
|
||||
|
||||
Widget lineChart({
|
||||
required BuildContext context,
|
||||
required String xAxisName,
|
||||
required String yAxisName,
|
||||
required String unit,
|
||||
required List<ChartData> data,
|
||||
}) {
|
||||
return SfCartesianChart(
|
||||
// 图表标题
|
||||
// title: ChartTitle(text: '2023年上半年销售额(万元)'),
|
||||
|
||||
// X轴配置(类别轴)
|
||||
primaryXAxis: CategoryAxis(majorGridLines: MajorGridLines(width: 0)),
|
||||
|
||||
// Y轴配置(数值轴)
|
||||
// primaryYAxis: NumericAxis(title: AxisTitle(text: '$yAxisName($unit)')),
|
||||
|
||||
// 启用图例
|
||||
legend: Legend(isVisible: true, position: LegendPosition.top),
|
||||
|
||||
// 启用交互提示(点击数据点显示详情)
|
||||
tooltipBehavior: TooltipBehavior(
|
||||
enable: true,
|
||||
format: 'point.x: point.y $unit',
|
||||
),
|
||||
|
||||
// 折线图数据系列
|
||||
series: [
|
||||
LineSeries<ChartData, String>(
|
||||
dataSource: data,
|
||||
// X轴数据映射
|
||||
xValueMapper: (ChartData chart, _) => chart.name,
|
||||
// Y轴数据映射
|
||||
yValueMapper: (ChartData chart, _) => chart.value,
|
||||
// 线条颜色
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
// 线条宽度
|
||||
width: 3,
|
||||
|
||||
// 数据点样式
|
||||
markerSettings: const MarkerSettings(
|
||||
isVisible: true,
|
||||
color: Colors.white,
|
||||
shape: DataMarkerType.circle,
|
||||
height: 6,
|
||||
width: 6,
|
||||
),
|
||||
|
||||
// 折线名称(会显示在图例中)
|
||||
name: yAxisName,
|
||||
|
||||
// 启用数据标签(直接显示数值)
|
||||
dataLabelSettings: const DataLabelSettings(
|
||||
isVisible: true,
|
||||
color: Colors.white,
|
||||
opacity: 0,
|
||||
),
|
||||
|
||||
// 动画效果
|
||||
animationDuration: 2000, // 动画时长(毫秒)
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget barChart({
|
||||
required BuildContext context,
|
||||
required String xAxisName,
|
||||
required String yAxisName,
|
||||
required String unit,
|
||||
required List<ChartData> data,
|
||||
}) {
|
||||
return SfCartesianChart(
|
||||
// X轴配置(类别轴)
|
||||
primaryXAxis: CategoryAxis(
|
||||
majorGridLines: MajorGridLines(width: 0),
|
||||
title: AxisTitle(text: xAxisName),
|
||||
),
|
||||
|
||||
// Y轴配置(数值轴)
|
||||
// primaryYAxis: NumericAxis(title: AxisTitle(text: '$yAxisName($unit)')),
|
||||
|
||||
// 启用交互提示
|
||||
tooltipBehavior: TooltipBehavior(
|
||||
enable: true,
|
||||
format: 'point.x: point.y $unit',
|
||||
),
|
||||
|
||||
// 柱状图数据系列
|
||||
series: [
|
||||
ColumnSeries<ChartData, String>(
|
||||
dataSource: data,
|
||||
// X轴数据映射
|
||||
xValueMapper: (ChartData chart, _) => chart.name,
|
||||
// Y轴数据映射
|
||||
yValueMapper: (ChartData chart, _) => chart.value,
|
||||
|
||||
// 名称
|
||||
name: yAxisName,
|
||||
|
||||
// 柱子颜色
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
|
||||
// 柱子宽度(0-1之间,1表示占满类别间隔)
|
||||
width: 0.6,
|
||||
|
||||
// 柱子边框
|
||||
borderWidth: 1,
|
||||
borderColor: Colors.black12,
|
||||
|
||||
// 数据标签
|
||||
dataLabelSettings: const DataLabelSettings(
|
||||
isVisible: true,
|
||||
color: Colors.white,
|
||||
opacity: 0,
|
||||
alignment: ChartAlignment.center,
|
||||
),
|
||||
|
||||
// 动画效果
|
||||
animationDuration: 2000,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget doubleBarChart({
|
||||
required BuildContext context,
|
||||
required String xAxisName,
|
||||
required String yAxisName,
|
||||
required String unit,
|
||||
required List<ChartData> data1,
|
||||
required List<ChartData> data2,
|
||||
required String series1Name,
|
||||
required String series2Name,
|
||||
}) {
|
||||
return SfCartesianChart(
|
||||
// X轴配置(类别轴)
|
||||
primaryXAxis: CategoryAxis(
|
||||
majorGridLines: const MajorGridLines(width: 0),
|
||||
title: AxisTitle(text: xAxisName),
|
||||
),
|
||||
|
||||
// Y轴配置(数值轴)
|
||||
// primaryYAxis: NumericAxis(title: AxisTitle(text: '$yAxisName($unit)')),
|
||||
|
||||
// 图例配置
|
||||
legend: Legend(
|
||||
isVisible: true,
|
||||
position: LegendPosition.top,
|
||||
overflowMode: LegendItemOverflowMode.wrap,
|
||||
),
|
||||
|
||||
// 启用交互提示
|
||||
tooltipBehavior: TooltipBehavior(
|
||||
enable: true,
|
||||
format: 'series.name: point.y $unit',
|
||||
),
|
||||
|
||||
// 双柱状图数据系列
|
||||
series: <ColumnSeries<ChartData, String>>[
|
||||
ColumnSeries<ChartData, String>(
|
||||
dataSource: data1,
|
||||
// X轴数据映射
|
||||
xValueMapper: (ChartData chart, _) => chart.name,
|
||||
// Y轴数据映射
|
||||
yValueMapper: (ChartData chart, _) => chart.value,
|
||||
// 系列名称
|
||||
name: series1Name,
|
||||
// 柱子颜色
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
// 柱子宽度
|
||||
width: 0.3,
|
||||
// 柱子边框
|
||||
borderWidth: 1,
|
||||
borderColor: Colors.black12,
|
||||
// 数据标签
|
||||
dataLabelSettings: const DataLabelSettings(
|
||||
isVisible: true,
|
||||
color: Colors.white,
|
||||
opacity: 0,
|
||||
alignment: ChartAlignment.center,
|
||||
),
|
||||
// 动画效果
|
||||
animationDuration: 2000,
|
||||
),
|
||||
ColumnSeries<ChartData, String>(
|
||||
dataSource: data2,
|
||||
// X轴数据映射
|
||||
xValueMapper: (ChartData chart, _) => chart.name,
|
||||
// Y轴数据映射
|
||||
yValueMapper: (ChartData chart, _) => chart.value,
|
||||
// 系列名称
|
||||
name: series2Name,
|
||||
// 柱子颜色
|
||||
color: Theme.of(context).colorScheme.inversePrimary,
|
||||
// 柱子宽度
|
||||
width: 0.3,
|
||||
// 柱子边框
|
||||
borderWidth: 1,
|
||||
borderColor: Colors.black12,
|
||||
// 数据标签
|
||||
dataLabelSettings: const DataLabelSettings(
|
||||
isVisible: true,
|
||||
color: Colors.white,
|
||||
opacity: 0,
|
||||
alignment: ChartAlignment.center,
|
||||
),
|
||||
// 动画效果
|
||||
animationDuration: 2000,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget pieChart({
|
||||
required BuildContext context,
|
||||
required String unit,
|
||||
required List<ChartData> data,
|
||||
}) {
|
||||
// 计算 value 的总和
|
||||
double sumValue = data.fold(0.0, (sum, item) => sum + item.value);
|
||||
|
||||
return SfCircularChart(
|
||||
// 饼图标题
|
||||
// title: ChartTitle(text: '菜谱类别占比分布'),
|
||||
|
||||
// 启用图例
|
||||
legend: const Legend(isVisible: true, position: LegendPosition.right),
|
||||
|
||||
// 启用交互提示(点击扇区显示详情)
|
||||
tooltipBehavior: TooltipBehavior(
|
||||
enable: true,
|
||||
format: 'point.x: point.y $unit',
|
||||
),
|
||||
|
||||
// 饼图系列配置
|
||||
series: [
|
||||
PieSeries<ChartData, String>(
|
||||
dataSource: data,
|
||||
// 类别映射(饼图扇区名称)
|
||||
xValueMapper: (ChartData data, _) => data.name,
|
||||
// 数值映射(扇区大小占比)
|
||||
yValueMapper: (ChartData data, _) => data.value,
|
||||
|
||||
// 扇区半径(0-1之间,1表示充满容器)
|
||||
// radius: '50%',
|
||||
|
||||
// 启用扇区分离效果
|
||||
explode: true,
|
||||
// 指定分离的扇区索引(这里分离第一个扇区)
|
||||
explodeIndex: 0,
|
||||
// 分离距离
|
||||
explodeOffset: '5%',
|
||||
|
||||
dataLabelMapper: (ChartData data, _) {
|
||||
final percentage = (data.value / sumValue * 100).toStringAsFixed(0);
|
||||
return '$percentage%';
|
||||
},
|
||||
|
||||
// 数据标签(显示在扇区上的文本)
|
||||
dataLabelSettings: DataLabelSettings(isVisible: true),
|
||||
|
||||
// 动画效果
|
||||
animationDuration: 2000,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user