statistical_charts.dart 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692
  1. // tools/statistical_charts.dart
  2. import 'package:flutter/material.dart';
  3. import 'package:fl_chart/fl_chart.dart';
  4. class StatisticalChartsPage extends StatefulWidget {
  5. @override
  6. _StatisticalChartsPageState createState() => _StatisticalChartsPageState();
  7. }
  8. class _StatisticalChartsPageState extends State<StatisticalChartsPage> {
  9. int _currentChartIndex = 0;
  10. final List<String> _chartTypes = ['条形图', '折线图', '饼图', '雷达图'];
  11. // 数据控制器
  12. final List<TextEditingController> _dataControllers = [];
  13. final List<TextEditingController> _labelControllers = [];
  14. // 图表数据
  15. List<double> _dataValues = [];
  16. List<String> _dataLabels = [];
  17. @override
  18. void initState() {
  19. super.initState();
  20. // 初始化默认数据
  21. _initializeDefaultData();
  22. }
  23. void _initializeDefaultData() {
  24. // 清空现有控制器
  25. _dataControllers.clear();
  26. _labelControllers.clear();
  27. // 添加默认数据
  28. for (int i = 0; i < 5; i++) {
  29. _dataControllers.add(TextEditingController(text: '${(i + 1) * 10}'));
  30. _labelControllers.add(TextEditingController(text: '数据${i + 1}'));
  31. }
  32. _updateChartData();
  33. }
  34. void _updateChartData() {
  35. setState(() {
  36. _dataValues = [];
  37. _dataLabels = [];
  38. for (int i = 0; i < _dataControllers.length; i++) {
  39. if (_dataControllers[i].text.isNotEmpty &&
  40. _labelControllers[i].text.isNotEmpty) {
  41. try {
  42. double value = double.parse(_dataControllers[i].text);
  43. _dataValues.add(value);
  44. _dataLabels.add(_labelControllers[i].text);
  45. } catch (e) {
  46. // 忽略无效数据
  47. }
  48. }
  49. }
  50. });
  51. }
  52. void _addDataField() {
  53. setState(() {
  54. _dataControllers.add(TextEditingController(text: '0'));
  55. _labelControllers.add(TextEditingController(text: '新数据'));
  56. });
  57. }
  58. void _removeDataField(int index) {
  59. if (_dataControllers.length > 1) {
  60. setState(() {
  61. _dataControllers.removeAt(index);
  62. _labelControllers.removeAt(index);
  63. _updateChartData();
  64. });
  65. }
  66. }
  67. Widget _buildDataInputPanel() {
  68. return Card(
  69. elevation: 4,
  70. margin: EdgeInsets.all(16),
  71. child: Padding(
  72. padding: EdgeInsets.all(16),
  73. child: Column(
  74. crossAxisAlignment: CrossAxisAlignment.start,
  75. children: [
  76. Text(
  77. '数据输入',
  78. style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
  79. ),
  80. SizedBox(height: 16),
  81. Row(
  82. children: [
  83. Expanded(
  84. flex: 2,
  85. child: Text(
  86. '标签',
  87. style: TextStyle(fontWeight: FontWeight.bold),
  88. textAlign: TextAlign.center,
  89. ),
  90. ),
  91. SizedBox(width: 16),
  92. Expanded(
  93. flex: 2,
  94. child: Text(
  95. '数值',
  96. style: TextStyle(fontWeight: FontWeight.bold),
  97. textAlign: TextAlign.center,
  98. ),
  99. ),
  100. SizedBox(width: 16),
  101. Expanded(
  102. flex: 1,
  103. child: Text(
  104. '操作',
  105. style: TextStyle(fontWeight: FontWeight.bold),
  106. textAlign: TextAlign.center,
  107. ),
  108. ),
  109. ],
  110. ),
  111. SizedBox(height: 8),
  112. ...List.generate(_dataControllers.length, (index) {
  113. return Padding(
  114. padding: EdgeInsets.symmetric(vertical: 8),
  115. child: Row(
  116. children: [
  117. Expanded(
  118. flex: 2,
  119. child: TextField(
  120. controller: _labelControllers[index],
  121. decoration: InputDecoration(
  122. hintText: '输入标签',
  123. border: OutlineInputBorder(),
  124. contentPadding: EdgeInsets.symmetric(horizontal: 12),
  125. ),
  126. onChanged: (value) => _updateChartData(),
  127. ),
  128. ),
  129. SizedBox(width: 16),
  130. Expanded(
  131. flex: 2,
  132. child: TextField(
  133. controller: _dataControllers[index],
  134. decoration: InputDecoration(
  135. hintText: '输入数值',
  136. border: OutlineInputBorder(),
  137. contentPadding: EdgeInsets.symmetric(horizontal: 12),
  138. ),
  139. keyboardType: TextInputType.number,
  140. onChanged: (value) => _updateChartData(),
  141. ),
  142. ),
  143. SizedBox(width: 16),
  144. Expanded(
  145. flex: 1,
  146. child: IconButton(
  147. icon: Icon(Icons.delete, color: Colors.red),
  148. onPressed: () => _removeDataField(index),
  149. ),
  150. ),
  151. ],
  152. ),
  153. );
  154. }),
  155. SizedBox(height: 16),
  156. Row(
  157. children: [
  158. ElevatedButton(
  159. onPressed: _addDataField,
  160. child: Text('添加数据'),
  161. style: ElevatedButton.styleFrom(
  162. backgroundColor: Colors.blue,
  163. foregroundColor: Colors.white,
  164. ),
  165. ),
  166. SizedBox(width: 16),
  167. ElevatedButton(
  168. onPressed: _initializeDefaultData,
  169. child: Text('重置数据'),
  170. style: ElevatedButton.styleFrom(
  171. backgroundColor: Colors.grey,
  172. foregroundColor: Colors.white,
  173. ),
  174. ),
  175. ],
  176. ),
  177. ],
  178. ),
  179. ),
  180. );
  181. }
  182. Widget _buildChartTypeSelector() {
  183. return Card(
  184. elevation: 4,
  185. margin: EdgeInsets.symmetric(horizontal: 16),
  186. child: Padding(
  187. padding: EdgeInsets.all(16),
  188. child: Column(
  189. crossAxisAlignment: CrossAxisAlignment.start,
  190. children: [
  191. Text(
  192. '图表类型',
  193. style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
  194. ),
  195. SizedBox(height: 12),
  196. Wrap(
  197. spacing: 8,
  198. runSpacing: 8,
  199. children: List.generate(_chartTypes.length, (index) {
  200. return ChoiceChip(
  201. label: Text(_chartTypes[index]),
  202. selected: _currentChartIndex == index,
  203. onSelected: (selected) {
  204. setState(() {
  205. _currentChartIndex = index;
  206. });
  207. },
  208. selectedColor: Colors.blue,
  209. labelStyle: TextStyle(
  210. color: _currentChartIndex == index ? Colors.white : Colors.black,
  211. ),
  212. );
  213. }),
  214. ),
  215. ],
  216. ),
  217. ),
  218. );
  219. }
  220. Widget _buildChart() {
  221. if (_dataValues.isEmpty) {
  222. return Container(
  223. height: 300,
  224. child: Center(
  225. child: Text(
  226. '请输入有效数据',
  227. style: TextStyle(fontSize: 18, color: Colors.grey),
  228. ),
  229. ),
  230. );
  231. }
  232. switch (_currentChartIndex) {
  233. case 0: // 条形图
  234. return _buildBarChart();
  235. case 1: // 折线图
  236. return _buildLineChart();
  237. case 2: // 饼图
  238. return _buildPieChart();
  239. case 3: // 雷达图
  240. return _buildRadarChart();
  241. default:
  242. return _buildBarChart();
  243. }
  244. }
  245. Widget _buildBarChart() {
  246. return Card(
  247. elevation: 4,
  248. margin: EdgeInsets.all(16),
  249. child: Padding(
  250. padding: EdgeInsets.all(16),
  251. child: Column(
  252. children: [
  253. Text(
  254. '条形图',
  255. style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
  256. ),
  257. SizedBox(height: 16),
  258. Container(
  259. height: 300,
  260. child: BarChart(
  261. BarChartData(
  262. alignment: BarChartAlignment.spaceAround,
  263. maxY: _dataValues.isEmpty ? 100 : _dataValues.reduce((a, b) => a > b ? a : b) * 1.2,
  264. barTouchData: BarTouchData(
  265. enabled: true,
  266. // 简化 tooltip 配置
  267. touchTooltipData: BarTouchTooltipData(),
  268. ),
  269. titlesData: FlTitlesData(
  270. show: true,
  271. bottomTitles: AxisTitles(
  272. sideTitles: SideTitles(
  273. showTitles: true,
  274. getTitlesWidget: (value, meta) {
  275. int index = value.toInt();
  276. if (index >= 0 && index < _dataLabels.length) {
  277. return Padding(
  278. padding: const EdgeInsets.only(top: 8.0),
  279. child: Text(
  280. _dataLabels[index],
  281. style: TextStyle(fontSize: 10),
  282. maxLines: 2,
  283. overflow: TextOverflow.ellipsis,
  284. ),
  285. );
  286. }
  287. return Text('');
  288. },
  289. ),
  290. ),
  291. leftTitles: AxisTitles(
  292. sideTitles: SideTitles(
  293. showTitles: true,
  294. reservedSize: 40,
  295. ),
  296. ),
  297. ),
  298. gridData: FlGridData(show: true),
  299. borderData: FlBorderData(show: true),
  300. barGroups: List.generate(_dataValues.length, (index) {
  301. return BarChartGroupData(
  302. x: index,
  303. barRods: [
  304. BarChartRodData(
  305. toY: _dataValues[index],
  306. color: Colors.primaries[index % Colors.primaries.length],
  307. width: 16,
  308. ),
  309. ],
  310. );
  311. }),
  312. ),
  313. ),
  314. ),
  315. ],
  316. ),
  317. ),
  318. );
  319. }
  320. // 在类顶部添加曲线类型状态变量
  321. bool _isLineCurved = true;
  322. Widget _buildLineChart() {
  323. return Card(
  324. elevation: 4,
  325. margin: EdgeInsets.all(16),
  326. child: Padding(
  327. padding: EdgeInsets.all(16),
  328. child: Column(
  329. children: [
  330. Text(
  331. '折线图',
  332. style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
  333. ),
  334. SizedBox(height: 16),
  335. // 添加曲线类型选择器
  336. Container(
  337. padding: EdgeInsets.symmetric(horizontal: 16),
  338. child: Row(
  339. mainAxisAlignment: MainAxisAlignment.spaceBetween,
  340. children: [
  341. Text(
  342. '曲线类型:',
  343. style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500),
  344. ),
  345. Row(
  346. children: [
  347. ChoiceChip(
  348. label: Text('直线'),
  349. selected: !_isLineCurved,
  350. onSelected: (selected) {
  351. setState(() {
  352. _isLineCurved = false;
  353. });
  354. },
  355. selectedColor: Colors.blue,
  356. labelStyle: TextStyle(
  357. color: !_isLineCurved ? Colors.white : Colors.black,
  358. ),
  359. ),
  360. SizedBox(width: 8),
  361. ChoiceChip(
  362. label: Text('光滑曲线'),
  363. selected: _isLineCurved,
  364. onSelected: (selected) {
  365. setState(() {
  366. _isLineCurved = true;
  367. });
  368. },
  369. selectedColor: Colors.blue,
  370. labelStyle: TextStyle(
  371. color: _isLineCurved ? Colors.white : Colors.black,
  372. ),
  373. ),
  374. ],
  375. ),
  376. ],
  377. ),
  378. ),
  379. SizedBox(height: 16),
  380. Container(
  381. height: 300,
  382. child: LineChart(
  383. LineChartData(
  384. lineTouchData: LineTouchData(enabled: true),
  385. gridData: FlGridData(show: true),
  386. titlesData: FlTitlesData(
  387. show: true,
  388. bottomTitles: AxisTitles(
  389. sideTitles: SideTitles(
  390. showTitles: true,
  391. getTitlesWidget: (value, meta) {
  392. int index = value.toInt();
  393. if (index >= 0 && index < _dataLabels.length) {
  394. return Padding(
  395. padding: const EdgeInsets.only(top: 8.0),
  396. child: Text(
  397. _dataLabels[index],
  398. style: TextStyle(fontSize: 10),
  399. ),
  400. );
  401. }
  402. return Text('');
  403. },
  404. ),
  405. ),
  406. leftTitles: AxisTitles(
  407. sideTitles: SideTitles(
  408. showTitles: true,
  409. reservedSize: 40,
  410. ),
  411. ),
  412. ),
  413. borderData: FlBorderData(show: true),
  414. minX: 0,
  415. maxX: _dataValues.length > 0 ? (_dataValues.length - 1).toDouble() : 1,
  416. minY: 0,
  417. maxY: _dataValues.isEmpty ? 100 : _dataValues.reduce((a, b) => a > b ? a : b) * 1.2,
  418. lineBarsData: [
  419. LineChartBarData(
  420. spots: List.generate(_dataValues.length, (index) {
  421. return FlSpot(index.toDouble(), _dataValues[index]);
  422. }),
  423. isCurved: _isLineCurved,
  424. color: Colors.blue,
  425. barWidth: 4,
  426. belowBarData: BarAreaData(show: false),
  427. dotData: FlDotData(
  428. show: true,
  429. getDotPainter: (spot, percent, barData, index) {
  430. return FlDotCirclePainter(
  431. radius: 4,
  432. color: Colors.blue,
  433. strokeWidth: 2,
  434. strokeColor: Colors.white,
  435. );
  436. },
  437. ),
  438. ),
  439. ],
  440. ),
  441. ),
  442. ),
  443. ],
  444. ),
  445. ),
  446. );
  447. }
  448. Widget _buildPieChart() {
  449. double total = _dataValues.fold(0, (a, b) => a + b);
  450. return Card(
  451. elevation: 4,
  452. margin: EdgeInsets.all(16),
  453. child: Padding(
  454. padding: EdgeInsets.all(16),
  455. child: Column(
  456. children: [
  457. Text(
  458. '饼图',
  459. style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
  460. ),
  461. SizedBox(height: 16),
  462. Container(
  463. height: 300,
  464. child: Row(
  465. children: [
  466. Expanded(
  467. flex: 2,
  468. child: PieChart(
  469. PieChartData(
  470. sectionsSpace: 4,
  471. centerSpaceRadius: 40,
  472. sections: List.generate(_dataValues.length, (index) {
  473. return PieChartSectionData(
  474. color: Colors.primaries[index % Colors.primaries.length],
  475. value: _dataValues[index],
  476. title: '${(_dataValues[index] / total * 100).toStringAsFixed(1)}%',
  477. radius: 50,
  478. titleStyle: TextStyle(
  479. fontSize: 12,
  480. fontWeight: FontWeight.bold,
  481. color: Colors.white,
  482. ),
  483. );
  484. }),
  485. ),
  486. ),
  487. ),
  488. Expanded(
  489. flex: 1,
  490. child: Column(
  491. mainAxisAlignment: MainAxisAlignment.center,
  492. crossAxisAlignment: CrossAxisAlignment.start,
  493. children: List.generate(_dataValues.length, (index) {
  494. return Padding(
  495. padding: EdgeInsets.symmetric(vertical: 4),
  496. child: Row(
  497. children: [
  498. Container(
  499. width: 12,
  500. height: 12,
  501. color: Colors.primaries[index % Colors.primaries.length],
  502. ),
  503. SizedBox(width: 8),
  504. Flexible(
  505. child: Text(
  506. _dataLabels[index],
  507. style: TextStyle(fontSize: 12),
  508. overflow: TextOverflow.ellipsis,
  509. ),
  510. ),
  511. ],
  512. ),
  513. );
  514. }),
  515. ),
  516. ),
  517. ],
  518. ),
  519. ),
  520. ],
  521. ),
  522. ),
  523. );
  524. }
  525. Widget _buildRadarChart() {
  526. if (_dataValues.length < 3) {
  527. return Card(
  528. elevation: 4,
  529. margin: EdgeInsets.all(16),
  530. child: Padding(
  531. padding: EdgeInsets.all(16),
  532. child: Column(
  533. children: [
  534. Text(
  535. '雷达图',
  536. style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
  537. ),
  538. SizedBox(height: 16),
  539. Container(
  540. height: 300,
  541. child: Center(
  542. child: Text(
  543. '雷达图需要至少3个数据点\n请添加更多数据',
  544. textAlign: TextAlign.center,
  545. style: TextStyle(fontSize: 16, color: Colors.grey),
  546. ),
  547. ),
  548. ),
  549. ],
  550. ),
  551. ),
  552. );
  553. }
  554. return Card(
  555. elevation: 4,
  556. margin: EdgeInsets.all(16),
  557. child: Padding(
  558. padding: EdgeInsets.all(16),
  559. child: Column(
  560. children: [
  561. Text(
  562. '雷达图',
  563. style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
  564. ),
  565. SizedBox(height: 16),
  566. Container(
  567. height: 300,
  568. child: RadarChart(
  569. RadarChartData(
  570. dataSets: [
  571. RadarDataSet(
  572. dataEntries: List.generate(_dataValues.length, (index) {
  573. return RadarEntry(
  574. value: _dataValues[index],
  575. );
  576. }),
  577. fillColor: Colors.blue.withOpacity(0.3),
  578. borderColor: Colors.blue,
  579. borderWidth: 2,
  580. ),
  581. ],
  582. radarBackgroundColor: Colors.transparent,
  583. radarBorderData: BorderSide(color: Colors.grey, width: 1),
  584. tickBorderData: BorderSide(color: Colors.grey, width: 1),
  585. gridBorderData: BorderSide(color: Colors.grey, width: 1),
  586. ticksTextStyle: TextStyle(color: Colors.black, fontSize: 10),
  587. tickCount: 5,
  588. getTitle: (index, angle) {
  589. if (index >= 0 && index < _dataLabels.length) {
  590. return RadarChartTitle(
  591. text: _dataLabels[index],
  592. angle: angle,
  593. );
  594. }
  595. return RadarChartTitle(text: '');
  596. },
  597. titleTextStyle: TextStyle(
  598. color: Colors.black,
  599. fontSize: 12,
  600. fontWeight: FontWeight.bold,
  601. ),
  602. titlePositionPercentageOffset: 0.15,
  603. ),
  604. swapAnimationDuration: Duration(milliseconds: 500),
  605. swapAnimationCurve: Curves.easeInOut,
  606. ),
  607. ),
  608. SizedBox(height: 16),
  609. Container(
  610. height: 20,
  611. child: Row(
  612. mainAxisAlignment: MainAxisAlignment.center,
  613. children: [
  614. Container(
  615. width: 12,
  616. height: 12,
  617. decoration: BoxDecoration(
  618. color: Colors.blue.withOpacity(0.3),
  619. border: Border.all(color: Colors.blue, width: 2),
  620. ),
  621. ),
  622. SizedBox(width: 8),
  623. Text(
  624. '数据分布',
  625. style: TextStyle(fontSize: 12, color: Colors.grey),
  626. ),
  627. ],
  628. ),
  629. ),
  630. ],
  631. ),
  632. ),
  633. );
  634. }
  635. @override
  636. Widget build(BuildContext context) {
  637. return Scaffold(
  638. appBar: AppBar(
  639. title: Text('统计图表绘制', style: TextStyle(color: Colors.black)),
  640. backgroundColor: Colors.white,
  641. elevation: 1,
  642. ),
  643. body: SingleChildScrollView(
  644. child: Column(
  645. children: [
  646. _buildChartTypeSelector(),
  647. _buildDataInputPanel(),
  648. _buildChart(),
  649. ],
  650. ),
  651. ),
  652. );
  653. }
  654. @override
  655. void dispose() {
  656. // 清理控制器
  657. for (var controller in _dataControllers) {
  658. controller.dispose();
  659. }
  660. for (var controller in _labelControllers) {
  661. controller.dispose();
  662. }
  663. super.dispose();
  664. }
  665. }