350 lines
9.4 KiB
Dart
350 lines
9.4 KiB
Dart
import 'package:blog_app/models/common.dart';
|
||
import 'package:flutter/material.dart';
|
||
import 'package:syncfusion_flutter_charts/charts.dart';
|
||
|
||
Widget buildChartTitle(BuildContext context, String title, IconData icon) {
|
||
return Row(
|
||
children: [
|
||
Icon(icon, 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)),
|
||
|
||
// 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)),
|
||
|
||
// 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,
|
||
),
|
||
],
|
||
);
|
||
}
|
||
|
||
Color _getRankColor(BuildContext context, int index) {
|
||
switch (index) {
|
||
case 0: // 金牌
|
||
return const Color(0xFFFFD700); // 标准金色
|
||
case 1: // 银牌
|
||
return const Color(0xFFC0C0C0); // 标准银色
|
||
case 2: // 铜牌
|
||
return const Color(0xFFCD7F32); // 标准铜色
|
||
default:
|
||
return Theme.of(context).colorScheme.onSurface; // 普通颜色
|
||
}
|
||
}
|
||
|
||
Widget rankChart({
|
||
required BuildContext context,
|
||
required String unit,
|
||
required List<ChartData> data,
|
||
}) {
|
||
return ListView.builder(
|
||
itemCount: data.length,
|
||
itemBuilder: (context, index) {
|
||
final item = data[index];
|
||
final rank = index + 1;
|
||
return SizedBox(
|
||
height: 35,
|
||
child: Row(
|
||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||
children: [
|
||
Row(
|
||
children: [
|
||
Container(
|
||
width: 25,
|
||
alignment: Alignment.center,
|
||
child: Text(
|
||
'$rank.',
|
||
style: TextStyle(
|
||
fontWeight: FontWeight.bold,
|
||
color: _getRankColor(context, index),
|
||
),
|
||
),
|
||
),
|
||
Text(
|
||
item.name,
|
||
style: TextStyle(
|
||
fontWeight: index < 3 ? FontWeight.bold : FontWeight.normal,
|
||
color: _getRankColor(context, index),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
Text(
|
||
'${item.value.toStringAsFixed(0)} $unit',
|
||
style: TextStyle(
|
||
fontWeight: index < 3 ? FontWeight.bold : FontWeight.normal,
|
||
color: _getRankColor(context, index),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
},
|
||
);
|
||
}
|