Flutter 图表组件学习指南
Flutter提供了多种图表库解决方案,可以满足从简单图表到复杂数据可视化的需求。以下是主要图表库的详细介绍。
一、图表库选择概览
| 库名称 | 特点 | 适用场景 | 许可证 |
|---|---|---|---|
| fl_chart | 轻量、灵活、可定制性强 | 基础到中级图表需求 | MIT |
| syncfusion_flutter_charts | 功能丰富、企业级 | 复杂业务图表 | 免费版/商业版 |
| charts_flutter | Google官方出品 | 需要官方支持的项目 | Apache 2.0 |
| bezier_chart | 优雅的贝塞尔曲线 | 金融、趋势图 | MIT |
| flutter_sparkline | 极简小图表 | 迷你图表、趋势线 | BSD |
二、fl_chart - 最流行的Flutter图表库
安装
yaml
dependencies:
fl_chart: ^0.66.0
1. 折线图 (Line Chart)
基础折线图
dart
import 'package:flutter/material.dart';
import 'package:fl_chart/fl_chart.dart';
class BasicLineChartPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('基础折线图')),
body: Padding(
padding: EdgeInsets.all(16),
child: LineChart(
LineChartData(
gridData: FlGridData(show: false),
titlesData: FlTitlesData(show: false),
borderData: FlBorderData(show: false),
lineBarsData: [
LineChartBarData(
spots: [
FlSpot(0, 1),
FlSpot(1, 3),
FlSpot(2, 1.5),
FlSpot(3, 4),
FlSpot(4, 2),
FlSpot(5, 5),
FlSpot(6, 3.5),
],
isCurved: true,
color: Colors.blue,
barWidth: 3,
dotData: FlDotData(show: true),
belowBarData: BarAreaData(show: false),
),
],
),
),
),
);
}
}
高级折线图 - 多线条、交互
dart
class AdvancedLineChartPage extends StatefulWidget {
@override
_AdvancedLineChartPageState createState() => _AdvancedLineChartPageState();
}
class _AdvancedLineChartPageState extends State<AdvancedLineChartPage> {
int _touchedIndex = -1;
List<LineChartBarData> _getLineBarsData() {
return [
LineChartBarData(
spots: [
FlSpot(0, 3000),
FlSpot(1, 4500),
FlSpot(2, 3800),
FlSpot(3, 5200),
FlSpot(4, 4800),
FlSpot(5, 6000),
FlSpot(6, 5500),
],
isCurved: true,
colors: [Colors.blue],
barWidth: 4,
isStrokeCapRound: true,
dotData: FlDotData(
show: true,
getDotPainter: (spot, percent, barData, index) {
return FlDotCirclePainter(
radius: 4,
color: Colors.white,
strokeWidth: 2,
strokeColor: Colors.blue,
);
},
),
belowBarData: BarAreaData(
show: true,
colors: [Colors.blue.withOpacity(0.3)],
gradientFrom: Offset(0, 0),
gradientTo: Offset(0, 1),
),
),
LineChartBarData(
spots: [
FlSpot(0, 2000),
FlSpot(1, 3500),
FlSpot(2, 2800),
FlSpot(3, 4200),
FlSpot(4, 3800),
FlSpot(5, 5000),
FlSpot(6, 4500),
],
isCurved: true,
colors: [Colors.green],
barWidth: 4,
dotData: FlDotData(show: false),
dashArray: [5, 5],
),
];
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('高级折线图')),
body: Column(
children: [
// 图表
Expanded(
child: Padding(
padding: EdgeInsets.all(16),
child: LineChart(
LineChartData(
gridData: FlGridData(
show: true,
drawVerticalLine: true,
horizontalInterval: 1000,
verticalInterval: 1,
getDrawingHorizontalLine: (value) {
return FlLine(
color: Colors.grey[300]!,
strokeWidth: 1,
);
},
getDrawingVerticalLine: (value) {
return FlLine(
color: Colors.grey[300]!,
strokeWidth: 1,
);
},
),
titlesData: FlTitlesData(
show: true,
bottomTitles: SideTitles(
showTitles: true,
reservedSize: 22,
getTextStyles: (context, value) => TextStyle(
color: Colors.grey,
fontSize: 12,
),
getTitles: (value) {
switch (value.toInt()) {
case 0: return '周一';
case 1: return '周二';
case 2: return '周三';
case 3: return '周四';
case 4: return '周五';
case 5: return '周六';
case 6: return '周日';
default: return '';
}
},
margin: 8,
),
leftTitles: SideTitles(
showTitles: true,
getTextStyles: (context, value) => TextStyle(
color: Colors.grey,
fontSize: 12,
),
getTitles: (value) {
if (value == 0) return '0';
if (value == 2000) return '2k';
if (value == 4000) return '4k';
if (value == 6000) return '6k';
return '';
},
reservedSize: 28,
margin: 12,
),
),
borderData: FlBorderData(
show: true,
border: Border.all(color: Colors.grey[300]!, width: 1),
),
minX: 0,
maxX: 6,
minY: 0,
maxY: 7000,
lineTouchData: LineTouchData(
touchTooltipData: LineTouchTooltipData(
tooltipBgColor: Colors.blueGrey,
getTooltipItems: (touchedSpots) {
return touchedSpots.map((touchedSpot) {
return LineTooltipItem(
'¥${touchedSpot.y.toInt()}',
TextStyle(color: Colors.white),
);
}).toList();
},
),
touchCallback: (event, touchResponse) {
if (event is FlTapUpEvent || touchResponse == null) {
setState(() {
_touchedIndex = -1;
});
return;
}
setState(() {
_touchedIndex = touchResponse.lineBarSpots?.first.barIndex ?? -1;
});
},
),
lineBarsData: _getLineBarsData(),
),
),
),
),
// 图例
Container(
height: 60,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
_buildLegend(Colors.blue, '当前周'),
SizedBox(width: 20),
_buildLegend(Colors.green, '上周'),
],
),
),
],
),
);
}
Widget _buildLegend(Color color, String text) {
return Row(
children: [
Container(
width: 12,
height: 12,
decoration: BoxDecoration(
color: color,
borderRadius: BorderRadius.circular(2),
),
),
SizedBox(width: 8),
Text(text, style: TextStyle(color: Colors.grey[700])),
],
);
}
}
实时数据折线图
dart
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:fl_chart/fl_chart.dart';
class RealTimeLineChartPage extends StatefulWidget {
@override
_RealTimeLineChartPageState createState() => _RealTimeLineChartPageState();
}
class _RealTimeLineChartPageState extends State<RealTimeLineChartPage> {
late Timer _timer;
List<FlSpot> _spots = [];
int _counter = 0;
double _maxY = 100;
@override
void initState() {
super.initState();
_startTimer();
}
void _startTimer() {
_timer = Timer.periodic(Duration(milliseconds: 500), (timer) {
setState(() {
// 生成随机数据
double newValue = 50 + Random().nextDouble() * 50;
_spots.add(FlSpot(_counter.toDouble(), newValue));
_counter++;
// 限制数据点数量
if (_spots.length > 30) {
_spots.removeAt(0);
// 更新所有点的x坐标
for (int i = 0; i < _spots.length; i++) {
_spots[i] = FlSpot(i.toDouble(), _spots[i].y);
}
_counter = _spots.length;
}
// 更新Y轴最大值
if (newValue > _maxY) {
_maxY = newValue + 10;
}
});
});
}
@override
void dispose() {
_timer.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('实时数据折线图')),
body: Column(
children: [
// 图表
Expanded(
child: Padding(
padding: EdgeInsets.all(16),
child: LineChart(
LineChartData(
gridData: FlGridData(show: false),
titlesData: FlTitlesData(show: false),
borderData: FlBorderData(show: false),
minX: 0,
maxX: _counter.toDouble(),
minY: 0,
maxY: _maxY,
lineBarsData: [
LineChartBarData(
spots: _spots,
isCurved: true,
colors: [Colors.green],
barWidth: 2,
dotData: FlDotData(show: false),
belowBarData: BarAreaData(
show: true,
colors: [Colors.green.withOpacity(0.3)],
),
),
],
),
),
),
),
// 控制面板
Container(
padding: EdgeInsets.all(16),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
ElevatedButton(
onPressed: () {
setState(() {
_spots.clear();
_counter = 0;
_maxY = 100;
});
},
child: Text('重置'),
),
Text('数据点: ${_spots.length}', style: TextStyle(fontSize: 16)),
Text('最新值: ${_spots.isNotEmpty ? _spots.last.y.toStringAsFixed(2) : "0"}',
style: TextStyle(fontSize: 16)),
],
),
),
],
),
);
}
}
2. 饼图 (Pie Chart)
基础饼图
dart
class BasicPieChartPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('基础饼图')),
body: AspectRatio(
aspectRatio: 1.3,
child: PieChart(
PieChartData(
sectionsSpace: 0,
centerSpaceRadius: 40,
sections: [
PieChartSectionData(
color: Colors.blue,
value: 25,
title: '25%',
radius: 60,
titleStyle: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
PieChartSectionData(
color: Colors.green,
value: 35,
title: '35%',
radius: 60,
titleStyle: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
PieChartSectionData(
color: Colors.orange,
value: 20,
title: '20%',
radius: 60,
titleStyle: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
PieChartSectionData(
color: Colors.red,
value: 20,
title: '20%',
radius: 60,
titleStyle: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
],
),
),
),
);
}
}
交互式饼图
dart
class InteractivePieChartPage extends StatefulWidget {
@override
_InteractivePieChartPageState createState() => _InteractivePieChartPageState();
}
class _InteractivePieChartPageState extends State<InteractivePieChartPage> {
int _touchedIndex = -1;
final List<PieChartSectionData> _sections = [
PieChartSectionData(
color: Colors.blue,
value: 25,
title: '娱乐',
radius: 60,
titleStyle: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
PieChartSectionData(
color: Colors.green,
value: 35,
title: '餐饮',
radius: 60,
titleStyle: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
PieChartSectionData(
color: Colors.orange,
value: 20,
title: '交通',
radius: 60,
titleStyle: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
PieChartSectionData(
color: Colors.red,
value: 15,
title: '购物',
radius: 60,
titleStyle: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
PieChartSectionData(
color: Colors.purple,
value: 5,
title: '其他',
radius: 60,
titleStyle: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
];
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('交互式饼图')),
body: Column(
children: [
// 图表
Expanded(
child: Padding(
padding: EdgeInsets.all(16),
child: PieChart(
PieChartData(
pieTouchData: PieTouchData(
touchCallback: (pieTouchResponse) {
setState(() {
if (pieTouchResponse.touchInput is FlLongPressEnd ||
pieTouchResponse.touchInput is FlPanEnd) {
_touchedIndex = -1;
} else {
_touchedIndex = pieTouchResponse.touchedSectionIndex;
}
});
},
),
borderData: FlBorderData(show: false),
sectionsSpace: 2,
centerSpaceRadius: 60,
sections: _sections.map((section) {
final isTouched = _sections.indexOf(section) == _touchedIndex;
final radius = isTouched ? 70.0 : 60.0;
return PieChartSectionData(
color: section.color,
value: section.value,
title: section.title,
radius: radius,
titleStyle: section.titleStyle,
badgeWidget: isTouched
? Container(
padding: EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.white,
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: Colors.black26,
blurRadius: 4,
),
],
),
child: Text(
'${section.value}%',
style: TextStyle(
color: section.color,
fontWeight: FontWeight.bold,
),
),
)
: null,
badgePositionPercentageOffset: 0.98,
);
}).toList(),
),
),
),
),
// 图例
Container(
padding: EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('消费分类', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
SizedBox(height: 10),
Wrap(
spacing: 12,
runSpacing: 8,
children: _sections.map((section) {
return Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 12,
height: 12,
color: section.color,
),
SizedBox(width: 6),
Text(section.title!),
SizedBox(width: 4),
Text('${section.value}%',
style: TextStyle(fontWeight: FontWeight.bold)),
],
);
}).toList(),
),
],
),
),
],
),
);
}
}
3. 条形图 (Bar Chart)
基础条形图
dart
class BasicBarChartPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('基础条形图')),
body: Padding(
padding: EdgeInsets.all(16),
child: BarChart(
BarChartData(
alignment: BarChartAlignment.spaceAround,
maxY: 20,
barTouchData: BarTouchData(enabled: false),
titlesData: FlTitlesData(
show: true,
bottomTitles: SideTitles(
showTitles: true,
getTextStyles: (context, value) => TextStyle(
color: Colors.grey,
fontSize: 10,
),
margin: 10,
getTitles: (double value) {
switch (value.toInt()) {
case 0: return '一月';
case 1: return '二月';
case 2: return '三月';
case 3: return '四月';
case 4: return '五月';
case 5: return '六月';
default: return '';
}
},
),
leftTitles: SideTitles(
showTitles: true,
getTextStyles: (context, value) => TextStyle(
color: Colors.grey,
fontSize: 10,
),
margin: 10,
getTitles: (double value) {
if (value == 0) return '0';
if (value == 5) return '5';
if (value == 10) return '10';
if (value == 15) return '15';
if (value == 20) return '20';
return '';
},
),
),
gridData: FlGridData(
show: true,
checkToShowHorizontalLine: (value) => value % 5 == 0,
getDrawingHorizontalLine: (value) => FlLine(
color: Colors.grey[300]!,
strokeWidth: 1,
),
),
borderData: FlBorderData(
show: true,
border: Border.all(color: Colors.grey[400]!, width: 1),
),
barGroups: [
BarChartGroupData(
x: 0,
barRods: [
BarChartRodData(y: 8, colors: [Colors.blue]),
],
),
BarChartGroupData(
x: 1,
barRods: [
BarChartRodData(y: 10, colors: [Colors.blue]),
],
),
BarChartGroupData(
x: 2,
barRods: [
BarChartRodData(y: 14, colors: [Colors.blue]),
],
),
BarChartGroupData(
x: 3,
barRods: [
BarChartRodData(y: 16, colors: [Colors.blue]),
],
),
BarChartGroupData(
x: 4,
barRods: [
BarChartRodData(y: 13, colors: [Colors.blue]),
],
),
BarChartGroupData(
x: 5,
barRods: [
BarChartRodData(y: 10, colors: [Colors.blue]),
],
),
],
),
),
),
);
}
}
分组条形图
dart
class GroupedBarChartPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('分组条形图')),
body: Padding(
padding: EdgeInsets.all(16),
child: BarChart(
BarChartData(
alignment: BarChartAlignment.spaceBetween,
maxY: 100,
groupsSpace: 12,
barTouchData: BarTouchData(enabled: false),
titlesData: FlTitlesData(
show: true,
bottomTitles: SideTitles(
showTitles: true,
getTextStyles: (context, value) => TextStyle(
color: Colors.grey,
fontSize: 10,
),
margin: 10,
getTitles: (double value) {
switch (value.toInt()) {
case 0: return 'Q1';
case 1: return 'Q2';
case 2: return 'Q3';
case 3: return 'Q4';
default: return '';
}
},
),
leftTitles: SideTitles(
showTitles: true,
getTextStyles: (context, value) => TextStyle(
color: Colors.grey,
fontSize: 10,
),
margin: 10,
getTitles: (double value) {
if (value == 0) return '0';
if (value == 20) return '20';
if (value == 40) return '40';
if (value == 60) return '60';
if (value == 80) return '80';
if (value == 100) return '100';
return '';
},
),
),
borderData: FlBorderData(show: false),
barGroups: [
// 第一季度
BarChartGroupData(
x: 0,
barsSpace: 4,
barRods: [
BarChartRodData(
y: 45,
colors: [Colors.blue],
width: 12,
borderRadius: BorderRadius.circular(2),
),
BarChartRodData(
y: 65,
colors: [Colors.green],
width: 12,
borderRadius: BorderRadius.circular(2),
),
BarChartRodData(
y: 30,
colors: [Colors.orange],
width: 12,
borderRadius: BorderRadius.circular(2),
),
],
),
// 第二季度
BarChartGroupData(
x: 1,
barsSpace: 4,
barRods: [
BarChartRodData(
y: 50,
colors: [Colors.blue],
width: 12,
borderRadius: BorderRadius.circular(2),
),
BarChartRodData(
y: 70,
colors: [Colors.green],
width: 12,
borderRadius: BorderRadius.circular(2),
),
BarChartRodData(
y: 35,
colors: [Colors.orange],
width: 12,
borderRadius: BorderRadius.circular(2),
),
],
),
// 第三季度
BarChartGroupData(
x: 2,
barsSpace: 4,
barRods: [
BarChartRodData(
y: 60,
colors: [Colors.blue],
width: 12,
borderRadius: BorderRadius.circular(2),
),
BarChartRodData(
y: 85,
colors: [Colors.green],
width: 12,
borderRadius: BorderRadius.circular(2),
),
BarChartRodData(
y: 45,
colors: [Colors.orange],
width: 12,
borderRadius: BorderRadius.circular(2),
),
],
),
// 第四季度
BarChartGroupData(
x: 3,
barsSpace: 4,
barRods: [
BarChartRodData(
y: 70,
colors: [Colors.blue],
width: 12,
borderRadius: BorderRadius.circular(2),
),
BarChartRodData(
y: 95,
colors: [Colors.green],
width: 12,
borderRadius: BorderRadius.circular(2),
),
BarChartRodData(
y: 55,
colors: [Colors.orange],
width: 12,
borderRadius: BorderRadius.circular(2),
),
],
),
],
),
),
),
bottomNavigationBar: Container(
height: 60,
padding: EdgeInsets.all(8),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
_buildLegend(Colors.blue, '产品A'),
SizedBox(width: 16),
_buildLegend(Colors.green, '产品B'),
SizedBox(width: 16),
_buildLegend(Colors.orange, '产品C'),
],
),
),
);
}
Widget _buildLegend(Color color, String text) {
return Row(
children: [
Container(
width: 12,
height: 12,
decoration: BoxDecoration(
color: color,
borderRadius: BorderRadius.circular(2),
),
),
SizedBox(width: 6),
Text(text, style: TextStyle(fontSize: 12)),
],
);
}
}
4. 雷达图 (Radar Chart)
dart
class RadarChartPage extends StatefulWidget {
@override
_RadarChartPageState createState() => _RadarChartPageState();
}
class _RadarChartPageState extends State<RadarChartPage> {
int _selectedDataSet = 0;
List<RadarDataSet> _getDataSets() {
return [
RadarDataSet(
dataEntries: [
RadarEntry(value: 8),
RadarEntry(value: 6),
RadarEntry(value: 9),
RadarEntry(value: 7),
RadarEntry(value: 8),
RadarEntry(value: 7),
],
borderColor: Colors.blue,
fillColor: Colors.blue.withOpacity(0.3),
borderWidth: 2,
),
RadarDataSet(
dataEntries: [
RadarEntry(value: 6),
RadarEntry(value: 8),
RadarEntry(value: 7),
RadarEntry(value: 9),
RadarEntry(value: 6),
RadarEntry(value: 8),
],
borderColor: Colors.red,
fillColor: Colors.red.withOpacity(0.3),
borderWidth: 2,
dashPattern: [5, 5],
),
];
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('雷达图')),
body: Column(
children: [
// 图表
Expanded(
child: Padding(
padding: EdgeInsets.all(16),
child: RadarChart(
RadarChartData(
radarTouchData: RadarTouchData(
touchCallback: (response) {
if (response?.touchedSpot != null) {
setState(() {
_selectedDataSet = response!.touchedSpot!.touchedDataSetIndex;
});
}
},
),
dataSets: _getDataSets(),
radarBorderData: BorderSide(
color: Colors.grey[400]!,
width: 1,
),
tickBorderData: BorderSide(
color: Colors.grey[300]!,
width: 1,
),
gridBorderData: BorderSide(
color: Colors.grey[300]!,
width: 1,
),
titlePositionPercentageOffset: 0.2,
getTitle: (index, angle) {
final titles = ['技术', '沟通', '管理', '创新', '团队', '效率'];
return RadarChartTitle(
text: titles[index],
angle: angle,
);
},
tickCount: 5,
ticksTextStyle: TextStyle(color: Colors.grey, fontSize: 10),
titleTextStyle: TextStyle(color: Colors.grey[700]!, fontSize: 12),
),
),
),
),
// 控制面板
Container(
height: 80,
padding: EdgeInsets.all(16),
child: Column(
children: [
Text('选择数据集', style: TextStyle(fontSize: 16)),
SizedBox(height: 8),
ToggleButtons(
isSelected: [true, false],
onPressed: (index) {
setState(() {
_selectedDataSet = index;
});
},
children: [
Text('数据集 1'),
Text('数据集 2'),
],
),
],
),
),
],
),
);
}
}
三、syncfusion_flutter_charts - 企业级图表库
安装
yaml
dependencies:
syncfusion_flutter_charts: ^23.1.36
注意:需要添加许可证密钥(免费版可用)
dart
import 'package:flutter/material.dart';
import 'package:syncfusion_flutter_charts/charts.dart';
// 在main函数中或初始化时添加
void main() {
// SyncfusionLicense.registerLicense('你的许可证密钥');
runApp(MyApp());
}
1. 多种图表类型示例
dart
class SyncfusionChartsPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return DefaultTabController(
length: 5,
child: Scaffold(
appBar: AppBar(
title: Text('Syncfusion 图表集'),
bottom: TabBar(
tabs: [
Tab(text: '折线图'),
Tab(text: '柱状图'),
Tab(text: '饼图'),
Tab(text: '散点图'),
Tab(text: '金融图'),
],
),
),
body: TabBarView(
children: [
_buildLineChart(),
_buildBarChart(),
_buildPieChart(),
_buildScatterChart(),
_buildFinancialChart(),
],
),
),
);
}
Widget _buildLineChart() {
final List<ChartData> lineChartData = [
ChartData('Jan', 35),
ChartData('Feb', 28),
ChartData('Mar', 34),
ChartData('Apr', 32),
ChartData('May', 40),
];
return SfCartesianChart(
primaryXAxis: CategoryAxis(),
primaryYAxis: NumericAxis(minimum: 0, maximum: 50, interval: 10),
series: <ChartSeries>[
LineSeries<ChartData, String>(
dataSource: lineChartData,
xValueMapper: (ChartData data, _) => data.x,
yValueMapper: (ChartData data, _) => data.y,
markerSettings: MarkerSettings(isVisible: true),
dataLabelSettings: DataLabelSettings(isVisible: true),
),
],
tooltipBehavior: TooltipBehavior(enable: true),
);
}
Widget _buildBarChart() {
final List<ChartData> barChartData = [
ChartData('产品A', 120),
ChartData('产品B', 150),
ChartData('产品C', 80),
ChartData('产品D', 200),
ChartData('产品E', 90),
];
return SfCartesianChart(
primaryXAxis: CategoryAxis(),
primaryYAxis: NumericAxis(),
series: <ChartSeries>[
ColumnSeries<ChartData, String>(
dataSource: barChartData,
xValueMapper: (ChartData data, _) => data.x,
yValueMapper: (ChartData data, _) => data.y,
color: Colors.blue,
),
],
);
}
Widget _buildPieChart() {
final List<PieData> pieData = [
PieData('娱乐', 25, Colors.blue),
PieData('餐饮', 35, Colors.green),
PieData('交通', 20, Colors.orange),
PieData('购物', 15, Colors.red),
PieData('其他', 5, Colors.purple),
];
return SfCircularChart(
series: <CircularSeries>[
PieSeries<PieData, String>(
dataSource: pieData,
xValueMapper: (PieData data, _) => data.category,
yValueMapper: (PieData data, _) => data.value,
pointColorMapper: (PieData data, _) => data.color,
dataLabelSettings: DataLabelSettings(
isVisible: true,
labelPosition: ChartDataLabelPosition.outside,
),
),
],
legend: Legend(isVisible: true),
);
}
Widget _buildScatterChart() {
final List<ScatterData> scatterData = [
ScatterData(5, 25),
ScatterData(8, 32),
ScatterData(12, 45),
ScatterData(15, 30),
ScatterData(20, 60),
ScatterData(25, 55),
ScatterData(30, 70),
];
return SfCartesianChart(
primaryXAxis: NumericAxis(),
primaryYAxis: NumericAxis(),
series: <ChartSeries>[
ScatterSeries<ScatterData, double>(
dataSource: scatterData,
xValueMapper: (ScatterData data, _) => data.x,
yValueMapper: (ScatterData data, _) => data.y,
markerSettings: MarkerSettings(
width: 8,
height: 8,
),
),
],
);
}
Widget _buildFinancialChart() {
final List<FinancialData> financialData = [
FinancialData(DateTime(2023, 1, 1), 100, 110, 95, 105),
FinancialData(DateTime(2023, 1, 2), 105, 115, 100, 110),
FinancialData(DateTime(2023, 1, 3), 110, 120, 105, 115),
FinancialData(DateTime(2023, 1, 4), 115, 125, 110, 120),
FinancialData(DateTime(2023, 1, 5), 120, 130, 115, 125),
];
return SfCartesianChart(
primaryXAxis: DateTimeAxis(),
primaryYAxis: NumericAxis(),
series: <ChartSeries>[
HiloOpenCloseSeries<FinancialData, DateTime>(
dataSource: financialData,
xValueMapper: (FinancialData data, _) => data.date,
lowValueMapper: (FinancialData data, _) => data.low,
highValueMapper: (FinancialData data, _) => data.high,
openValueMapper: (FinancialData data, _) => data.open,
closeValueMapper: (FinancialData data, _) => data.close,
),
],
);
}
}
// 数据模型
class ChartData {
ChartData(this.x, this.y);
final String x;
final double y;
}
class PieData {
PieData(this.category, this.value, this.color);
final String category;
final double value;
final Color color;
}
class ScatterData {
ScatterData(this.x, this.y);
final double x;
final double y;
}
class FinancialData {
FinancialData(this.date, this.open, this.high, this.low, this.close);
final DateTime date;
final double open;
final double high;
final double low;
final double close;
}
2. 高级特性 - 动态更新
dart
class DynamicSyncfusionChart extends StatefulWidget {
@override
_DynamicSyncfusionChartState createState() => _DynamicSyncfusionChartState();
}
class _DynamicSyncfusionChartState extends State<DynamicSyncfusionChart> {
List<ChartData> _chartData = [];
late Timer _timer;
int _counter = 1;
@override
void initState() {
super.initState();
_initializeData();
_startTimer();
}
void _initializeData() {
_chartData = [
ChartData('点1', 10),
ChartData('点2', 15),
ChartData('点3', 12),
];
}
void _startTimer() {
_timer = Timer.periodic(Duration(seconds: 2), (timer) {
setState(() {
_counter++;
_chartData.add(ChartData('点${_counter + 2}', Random().nextDouble() * 20));
// 限制数据量
if (_chartData.length > 10) {
_chartData.removeAt(0);
}
});
});
}
@override
void dispose() {
_timer.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('动态更新图表')),
body: Column(
children: [
// 图表
Expanded(
child: SfCartesianChart(
primaryXAxis: CategoryAxis(),
primaryYAxis: NumericAxis(
minimum: 0,
maximum: 30,
interval: 5,
),
series: <ChartSeries>[
LineSeries<ChartData, String>(
dataSource: _chartData,
xValueMapper: (ChartData data, _) => data.x,
yValueMapper: (ChartData data, _) => data.y,
animationDuration: 1000,
markerSettings: MarkerSettings(isVisible: true),
),
],
tooltipBehavior: TooltipBehavior(enable: true),
),
),
// 控制面板
Container(
padding: EdgeInsets.all(16),
child: Column(
children: [
Text('数据点数量: ${_chartData.length}', style: TextStyle(fontSize: 16)),
SizedBox(height: 10),
ElevatedButton(
onPressed: () {
setState(() {
_chartData.clear();
_counter = 1;
_initializeData();
});
},
child: Text('重置数据'),
),
],
),
),
],
),
);
}
}
四、charts_flutter - Google官方图表库
安装
yaml
dependencies:
charts_flutter: ^0.12.0
基础使用示例
dart
import 'package:flutter/material.dart';
import 'package:charts_flutter/flutter.dart' as charts;
class GoogleChartsPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Google Charts')),
body: ListView(
padding: EdgeInsets.all(16),
children: [
_buildBarChart(),
SizedBox(height: 20),
_buildLineChart(),
SizedBox(height: 20),
_buildPieChart(),
],
),
);
}
Widget _buildBarChart() {
final data = [
ChartModel('周一', 35, Colors.blue),
ChartModel('周二', 28, Colors.green),
ChartModel('周三', 34, Colors.orange),
ChartModel('周四', 32, Colors.red),
ChartModel('周五', 40, Colors.purple),
];
final series = [
charts.Series<ChartModel, String>(
id: 'Sales',
domainFn: (ChartModel sales, _) => sales.day,
measureFn: (ChartModel sales, _) => sales.value,
colorFn: (ChartModel sales, _) => charts.ColorUtil.fromDartColor(sales.color),
data: data,
),
];
return Card(
child: Padding(
padding: EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('柱状图', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
SizedBox(height: 10),
Container(
height: 300,
child: charts.BarChart(
series,
animate: true,
vertical: false,
barRendererDecorator: charts.BarLabelDecorator<String>(),
domainAxis: charts.OrdinalAxisSpec(
renderSpec: charts.SmallTickRendererSpec(
labelRotation: 45,
),
),
),
),
],
),
),
);
}
Widget _buildLineChart() {
final data = [
TimeSeriesModel(DateTime(2023, 1, 1), 100),
TimeSeriesModel(DateTime(2023, 1, 2), 150),
TimeSeriesModel(DateTime(2023, 1, 3), 120),
TimeSeriesModel(DateTime(2023, 1, 4), 180),
TimeSeriesModel(DateTime(2023, 1, 5), 160),
];
final series = [
charts.Series<TimeSeriesModel, DateTime>(
id: 'Stock',
domainFn: (TimeSeriesModel sales, _) => sales.time,
measureFn: (TimeSeriesModel sales, _) => sales.value,
data: data,
),
];
return Card(
child: Padding(
padding: EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('折线图', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
SizedBox(height: 10),
Container(
height: 300,
child: charts.TimeSeriesChart(
series,
animate: true,
dateTimeFactory: const charts.LocalDateTimeFactory(),
),
),
],
),
),
);
}
Widget _buildPieChart() {
final data = [
PieModel('娱乐', 25, Colors.blue),
PieModel('餐饮', 35, Colors.green),
PieModel('交通', 20, Colors.orange),
PieModel('购物', 15, Colors.red),
PieModel('其他', 5, Colors.purple),
];
final series = [
charts.Series<PieModel, String>(
id: 'Expenses',
domainFn: (PieModel expense, _) => expense.category,
measureFn: (PieModel expense, _) => expense.value,
colorFn: (PieModel expense, _) => charts.ColorUtil.fromDartColor(expense.color),
data: data,
labelAccessorFn: (PieModel row, _) => '${row.category}: ${row.value}%',
),
];
return Card(
child: Padding(
padding: EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('饼图', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
SizedBox(height: 10),
Container(
height: 300,
child: charts.PieChart(
series,
animate: true,
defaultRenderer: charts.ArcRendererConfig(
arcWidth: 60,
arcRendererDecorators: [
charts.ArcLabelDecorator(
labelPosition: charts.ArcLabelPosition.inside,
),
],
),
),
),
],
),
),
);
}
}
// 数据模型
class ChartModel {
final String day;
final int value;
final Color color;
ChartModel(this.day, this.value, this.color);
}
class TimeSeriesModel {
final DateTime time;
final int value;
TimeSeriesModel(this.time, this.value);
}
class PieModel {
final String category;
final int value;
final Color color;
PieModel(this.category, this.value, this.color);
}
五、实用工具类 - 图表数据管理
dart
// 图表数据管理工具类
class ChartDataUtils {
// 生成模拟数据
static List<FlSpot> generateLineData({
int count = 7,
double minY = 0,
double maxY = 100,
double variance = 20,
}) {
final List<FlSpot> spots = [];
double currentY = (minY + maxY) / 2;
for (int i = 0; i < count; i++) {
// 添加一些随机波动
currentY += (Random().nextDouble() - 0.5) * variance;
currentY = currentY.clamp(minY, maxY);
spots.add(FlSpot(i.toDouble(), currentY));
}
return spots;
}
// 格式化时间标签
static List<String> generateTimeLabels(
DateTime startDate,
int count, {
Duration interval = const Duration(days: 1),
}) {
final List<String> labels = [];
DateTime currentDate = startDate;
for (int i = 0; i < count; i++) {
labels.add(_formatDate(currentDate));
currentDate = currentDate.add(interval);
}
return labels;
}
static String _formatDate(DateTime date) {
return '${date.month}/${date.day}';
}
// 计算统计信息
static Map<String, double> calculateStatistics(List<double> values) {
if (values.isEmpty) {
return {
'min': 0,
'max': 0,
'avg': 0,
'sum': 0,
};
}
final sum = values.reduce((a, b) => a + b);
final avg = sum / values.length;
return {
'min': values.reduce((a, b) => a < b ? a : b),
'max': values.reduce((a, b) => a > b ? a : b),
'avg': avg,
'sum': sum,
};
}
// 数据平滑处理
static List<double> smoothData(List<double> data, int windowSize) {
if (windowSize < 2) return data;
final List<double> smoothed = [];
for (int i = 0; i < data.length; i++) {
double sum = 0;
int count = 0;
for (int j = i - windowSize ~/ 2; j <= i + windowSize ~/ 2; j++) {
if (j >= 0 && j < data.length) {
sum += data[j];
count++;
}
}
smoothed.add(sum / count);
}
return smoothed;
}
}
// 图表主题管理
class ChartTheme {
static FlTitlesData getLightTitlesData() {
return FlTitlesData(
show: true,
bottomTitles: SideTitles(
showTitles: true,
reservedSize: 22,
getTextStyles: (context, value) => TextStyle(
color: Colors.grey[700],
fontSize: 12,
),
margin: 8,
),
leftTitles: SideTitles(
showTitles: true,
getTextStyles: (context, value) => TextStyle(
color: Colors.grey[700],
fontSize: 12,
),
reservedSize: 28,
margin: 12,
),
);
}
static FlTitlesData getDarkTitlesData() {
return FlTitlesData(
show: true,
bottomTitles: SideTitles(
showTitles: true,
reservedSize: 22,
getTextStyles: (context, value) => TextStyle(
color: Colors.grey[300],
fontSize: 12,
),
margin: 8,
),
leftTitles: SideTitles(
showTitles: true,
getTextStyles: (context, value) => TextStyle(
color: Colors.grey[300],
fontSize: 12,
),
reservedSize: 28,
margin: 12,
),
);
}
static PieChartSectionData createPieSection({
required double value,
required Color color,
required String title,
double radius = 60,
}) {
return PieChartSectionData(
color: color,
value: value,
title: '$value%',
radius: radius,
titleStyle: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Colors.white,
),
);
}
}
六、性能优化和最佳实践
1. 图表性能优化技巧
dart
class ChartPerformanceTips {
// 1. 限制数据点数量
static List<FlSpot> limitDataPoints(List<FlSpot> spots, int maxPoints) {
if (spots.length <= maxPoints) return spots;
final step = spots.length / maxPoints;
final List<FlSpot> limited = [];
for (int i = 0; i < maxPoints; i++) {
final index = (i * step).floor();
if (index < spots.length) {
limited.add(spots[index]);
}
}
return limited;
}
// 2. 使用缓存避免重复计算
static final Map<String, Widget> _chartCache = {};
static Widget cachedChart(
String key,
Widget Function() builder,
) {
if (_chartCache.containsKey(key)) {
return _chartCache[key]!;
}
final chart = builder();
_chartCache[key] = chart;
return chart;
}
// 3. 分块加载大数据集
static Widget lazyLoadChart(
List<FlSpot> allSpots,
int chunkSize,
Widget Function(List<FlSpot>) builder,
) {
return ListView.builder(
itemCount: (allSpots.length / chunkSize).ceil(),
itemBuilder: (context, index) {
final start = index * chunkSize;
final end = (index + 1) * chunkSize;
final chunk = allSpots.sublist(
start,
end < allSpots.length ? end : allSpots.length,
);
return builder(chunk);
},
);
}
// 4. 禁用不必要的功能
static LineChartData getMinimalLineChart(List<LineChartBarData> lineBars) {
return LineChartData(
gridData: FlGridData(show: false),
titlesData: FlTitlesData(show: false),
borderData: FlBorderData(show: false),
lineTouchData: LineTouchData(enabled: false),
clipData: FlClipData.none(),
lineBarsData: lineBars,
);
}
}
// 2. 图表响应式设计
class ResponsiveChart extends StatelessWidget {
final Widget Function(Size size) builder;
const ResponsiveChart({Key? key, required this.builder}) : super(key: key);
@override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (context, constraints) {
final size = Size(constraints.maxWidth, constraints.maxHeight);
return builder(size);
},
);
}
}
// 使用示例
class DashboardPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('仪表盘')),
body: ResponsiveChart(
builder: (size) {
if (size.width < 600) {
// 小屏幕:垂直布局
return ListView(
children: [
Container(
height: 200,
child: _buildMiniChart(),
),
Container(
height: 200,
child: _buildMiniChart2(),
),
],
);
} else {
// 大屏幕:网格布局
return GridView.count(
crossAxisCount: 2,
children: [
Container(
height: 300,
child: _buildFullChart(),
),
Container(
height: 300,
child: _buildFullChart2(),
),
Container(
height: 300,
child: _buildFullChart3(),
),
Container(
height: 300,
child: _buildFullChart4(),
),
],
);
}
},
),
);
}
Widget _buildMiniChart() {
return LineChart(
LineChartData(
lineBarsData: [
LineChartBarData(
spots: ChartDataUtils.generateLineData(count: 5),
isCurved: true,
colors: [Colors.blue],
),
],
),
);
}
Widget _buildFullChart() {
return LineChart(
LineChartData(
titlesData: ChartTheme.getLightTitlesData(),
borderData: FlBorderData(show: true),
lineBarsData: [
LineChartBarData(
spots: ChartDataUtils.generateLineData(count: 10),
isCurved: true,
colors: [Colors.blue],
),
],
),
);
}
}
七、实际应用案例
1. 股票K线图
dart
import 'package:flutter/material.dart';
import 'package:fl_chart/fl_chart.dart';
class StockCandlestickChart extends StatefulWidget {
@override
_StockCandlestickChartState createState() => _StockCandlestickChartState();
}
class _StockCandlestickChartState extends State<StockCandlestickChart> {
List<CandleData> _candleData = [];
bool _showVolume = false;
@override
void initState() {
super.initState();
_generateCandleData();
}
void _generateCandleData() {
// 模拟K线数据
final now = DateTime.now();
double lastClose = 100.0;
for (int i = 30; i >= 0; i--) {
final date = now.subtract(Duration(days: i));
final open = lastClose;
final high = open + Random().nextDouble() * 10;
final low = open - Random().nextDouble() * 10;
final close = low + Random().nextDouble() * (high - low);
final volume = 1000 + Random().nextInt(9000);
_candleData.add(CandleData(
date: date,
open: open,
high: high,
low: low,
close: close,
volume: volume.toDouble(),
));
lastClose = close;
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('股票K线图')),
body: Column(
children: [
// 控制面板
Container(
padding: EdgeInsets.all(12),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text('AAPL', style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold)),
Switch(
value: _showVolume,
onChanged: (value) {
setState(() {
_showVolume = value;
});
},
),
Text('显示成交量'),
],
),
),
// K线图
Expanded(
child: Padding(
padding: EdgeInsets.all(16),
child: LineChart(
LineChartData(
gridData: FlGridData(show: true),
titlesData: FlTitlesData(show: false),
borderData: FlBorderData(show: false),
minX: 0,
maxX: _candleData.length.toDouble() - 1,
minY: _getMinPrice(),
maxY: _getMaxPrice(),
lineBarsData: [
// 蜡烛图
LineChartBarData(
spots: _getCandleSpots(),
isCurved: false,
colors: [Colors.transparent],
barWidth: 4,
dotData: FlDotData(show: false),
belowBarData: BarAreaData(show: false),
),
],
lineTouchData: LineTouchData(
touchTooltipData: LineTouchTooltipData(
getTooltipItems: (touchedSpots) {
return touchedSpots.map((touchedSpot) {
final index = touchedSpot.x.toInt();
final data = _candleData[index];
return LineTooltipItem(
'日期: ${_formatDate(data.date)}\n'
'开: ${data.open.toStringAsFixed(2)}\n'
'高: ${data.high.toStringAsFixed(2)}\n'
'低: ${data.low.toStringAsFixed(2)}\n'
'收: ${data.close.toStringAsFixed(2)}\n'
'成交量: ${data.volume.toStringAsFixed(0)}',
TextStyle(color: Colors.white),
);
}).toList();
},
),
),
),
),
),
),
// 成交量图
if (_showVolume)
Container(
height: 100,
padding: EdgeInsets.all(16),
child: BarChart(
BarChartData(
alignment: BarChartAlignment.spaceBetween,
barTouchData: BarTouchData(enabled: false),
titlesData: FlTitlesData(show: false),
gridData: FlGridData(show: false),
borderData: FlBorderData(show: false),
barGroups: _getVolumeBars(),
),
),
),
],
),
);
}
List<FlSpot> _getCandleSpots() {
final List<FlSpot> spots = [];
for (int i = 0; i < _candleData.length; i++) {
final data = _candleData[i];
// 添加高低点
spots.add(FlSpot(i.toDouble(), data.low));
spots.add(FlSpot(i.toDouble(), data.high));
}
return spots;
}
List<BarChartGroupData> _getVolumeBars() {
return _candleData.asMap().entries.map((entry) {
final index = entry.key;
final data = entry.value;
final color = data.close > data.open ? Colors.green : Colors.red;
return BarChartGroupData(
x: index,
barRods: [
BarChartRodData(
y: data.volume / 1000, // 转换为千单位
colors: [color],
width: 8,
),
],
);
}).toList();
}
double _getMinPrice() {
return _candleData.map((data) => data.low).reduce((a, b) => a < b ? a : b) - 5;
}
double _getMaxPrice() {
return _candleData.map((data) => data.high).reduce((a, b) => a > b ? a : b) + 5;
}
String _formatDate(DateTime date) {
return '${date.month}/${date.day}';
}
}
class CandleData {
final DateTime date;
final double open;
final double high;
final double low;
final double close;
final double volume;
CandleData({
required this.date,
required this.open,
required this.high,
required this.low,
required this.close,
required this.volume,
});
}
八、总结与建议
选择建议
- 轻量级需求 :选择
fl_chart
- 开源免费
- 学习曲线平缓
- 社区活跃
- 企业级应用 :选择
syncfusion_flutter_charts
- 功能全面
- 技术支持好
- 性能优秀
- Google生态 :选择
charts_flutter
- 官方维护
- 稳定性好
- 文档规范
性能优化要点
- 数据层面:
- 限制数据点数量(1000个以内)
- 使用数据聚合(分时、分日)
- 避免频繁重绘
- 渲染层面:
- 使用
const构造函数 - 缓存图表组件
- 按需更新
- 交互层面:
- 懒加载大数据
- 防抖处理频繁交互
- 渐进式显示
最佳实践
- 响应式设计:根据屏幕尺寸调整图表布局
- 无障碍访问:为图表添加语义化描述
- 错误处理:处理数据异常和加载失败
- 主题适配:支持明暗主题切换
- 国际化:日期、货币格式本地化
通过合理选择图表库并遵循最佳实践,可以在Flutter应用中构建出功能强大、性能优越的数据可视化界面。