histogram.dart 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276
  1. // tools/histogram.dart
  2. import 'package:flutter/material.dart';
  3. import 'package:rational/rational.dart';
  4. import '../utils/math_utils.dart';
  5. import 'package:fl_chart/fl_chart.dart';
  6. class HistogramPage extends StatefulWidget {
  7. @override
  8. _HistogramPageState createState() => _HistogramPageState();
  9. }
  10. class _HistogramPageState extends State<HistogramPage> {
  11. final TextEditingController leftEndpointController = TextEditingController();
  12. final TextEditingController groupDistanceController = TextEditingController();
  13. final TextEditingController frequenciesController = TextEditingController();
  14. final TextEditingController percentileController = TextEditingController();
  15. String result = '';
  16. List<BarChartGroupData>? barGroups;
  17. List<String>? groupLabels;
  18. double? maxFrequencyDensity;
  19. @override
  20. Widget build(BuildContext context) {
  21. return Scaffold(
  22. appBar: AppBar(title: Text('直方图统计')),
  23. body: SingleChildScrollView( // 添加滚动视图
  24. padding: EdgeInsets.all(16),
  25. child: Column(
  26. children: [
  27. _buildInputField('左端点', leftEndpointController),
  28. _buildInputField('组距', groupDistanceController),
  29. _buildInputField('各组频率(用空格分隔)', frequenciesController),
  30. _buildInputField('百分位数(可选)', percentileController),
  31. SizedBox(height: 20),
  32. ElevatedButton(
  33. onPressed: _calculate,
  34. child: Text('计算'),
  35. ),
  36. SizedBox(height: 20),
  37. if (result.isNotEmpty)
  38. Card(
  39. child: Padding(
  40. padding: EdgeInsets.all(16),
  41. child: Text(
  42. result,
  43. style: TextStyle(fontSize: 14, fontFamily: 'Monospace'),
  44. ),
  45. ),
  46. ),
  47. if (barGroups != null && barGroups!.isNotEmpty)
  48. SizedBox( // 将 Expanded 改为 SizedBox 并指定高度
  49. height: 400, // 设置固定高度
  50. child: Card(
  51. child: Padding(
  52. padding: EdgeInsets.all(16),
  53. child: Column(
  54. children: [
  55. Text(
  56. '直方图',
  57. style: TextStyle(
  58. fontSize: 18,
  59. fontWeight: FontWeight.bold,
  60. ),
  61. ),
  62. SizedBox(height: 10),
  63. Expanded( // 这个 Expanded 可以保留,因为它是在 Column 内部
  64. child: BarChart(
  65. BarChartData(
  66. barGroups: barGroups,
  67. alignment: BarChartAlignment.spaceBetween,
  68. groupsSpace: 0,
  69. barTouchData: BarTouchData(
  70. enabled: false,
  71. ),
  72. borderData: FlBorderData(show: true),
  73. gridData: FlGridData(show: false),
  74. titlesData: FlTitlesData(
  75. bottomTitles: AxisTitles(
  76. sideTitles: SideTitles(
  77. showTitles: true,
  78. getTitlesWidget: (value, meta) {
  79. final index = value.toInt();
  80. if (index >= 0 && index < (groupLabels?.length ?? 0)) {
  81. return Padding(
  82. padding: const EdgeInsets.only(top: 8.0),
  83. child: Text(
  84. groupLabels![index],
  85. style: TextStyle(
  86. fontSize: 10,
  87. fontWeight: FontWeight.bold,
  88. ),
  89. textAlign: TextAlign.center,
  90. ),
  91. );
  92. }
  93. return Text('');
  94. },
  95. reservedSize: 40,
  96. ),
  97. ),
  98. leftTitles: AxisTitles(
  99. sideTitles: SideTitles(
  100. showTitles: true,
  101. reservedSize: 40,
  102. getTitlesWidget: (value, meta) {
  103. return Text(value.toStringAsFixed(2));
  104. },
  105. ),
  106. ),
  107. rightTitles: AxisTitles(
  108. sideTitles: SideTitles(showTitles: false),
  109. ),
  110. topTitles: AxisTitles(
  111. sideTitles: SideTitles(showTitles: false),
  112. ),
  113. ),
  114. maxY: maxFrequencyDensity != null
  115. ? (maxFrequencyDensity! * 1.1)
  116. : 1.0,
  117. ),
  118. ),
  119. ),
  120. ],
  121. ),
  122. ),
  123. ),
  124. ),
  125. ],
  126. ),
  127. ),
  128. );
  129. }
  130. Widget _buildInputField(String label, TextEditingController controller) {
  131. return Padding(
  132. padding: EdgeInsets.symmetric(vertical: 8),
  133. child: TextField(
  134. controller: controller,
  135. keyboardType: TextInputType.numberWithOptions(signed: true, decimal: true),
  136. decoration: InputDecoration(
  137. labelText: label,
  138. border: OutlineInputBorder(),
  139. hintText: label.contains('频率') ? '用空格分隔多个值' : '输入分数如: 1/2 或小数',
  140. ),
  141. ),
  142. );
  143. }
  144. void _calculate() {
  145. try {
  146. final a = MathUtils.parseRational(leftEndpointController.text);
  147. final d = MathUtils.parseRational(groupDistanceController.text);
  148. final frequencies = frequenciesController.text.split(' ').map((s) => MathUtils.parseRational(s)).toList();
  149. Rational sumFreq = MathUtils.zero;
  150. for (final f in frequencies) {
  151. sumFreq += f;
  152. }
  153. if (sumFreq != MathUtils.one) {
  154. setState(() {
  155. result = '错误: 频率之和必须为1,当前为 ${MathUtils.rationalToString(sumFreq)}';
  156. barGroups = null;
  157. groupLabels = null;
  158. });
  159. return;
  160. }
  161. // 计算均值
  162. Rational mean = MathUtils.zero;
  163. for (int i = 0; i < frequencies.length; i++) {
  164. mean += (a + MathUtils.fromInt(i) * d + d / MathUtils.two) * frequencies[i];
  165. }
  166. // 计算方差
  167. Rational variance = MathUtils.zero;
  168. for (int i = 0; i < frequencies.length; i++) {
  169. final midPoint = a + MathUtils.fromInt(i) * d + d / MathUtils.two;
  170. variance += (midPoint - mean).pow(2) * frequencies[i];
  171. }
  172. String output = '均值: ${MathUtils.rationalToString(mean)} (近似值: ${mean.toDouble()})\n'
  173. '方差: ${MathUtils.rationalToString(variance)} (近似值: ${variance.toDouble()})\n';
  174. // 计算中位数
  175. Rational cumulative = MathUtils.zero;
  176. for (int i = 0; i < frequencies.length; i++) {
  177. if (cumulative + frequencies[i] > MathUtils.half) {
  178. final median = a + MathUtils.fromInt(i) * d - (cumulative - MathUtils.half) / frequencies[i] * d;
  179. output += '中位数: ${MathUtils.rationalToString(median)} (近似值: ${median.toDouble()})\n';
  180. break;
  181. }
  182. cumulative += frequencies[i];
  183. }
  184. // 计算百分位数
  185. if (percentileController.text.isNotEmpty) {
  186. final p = MathUtils.parseRational(percentileController.text);
  187. cumulative = MathUtils.zero;
  188. for (int i = 0; i < frequencies.length; i++) {
  189. if (cumulative + frequencies[i] > p) {
  190. final percentile = a + MathUtils.fromInt(i) * d - (cumulative - p) / frequencies[i] * d;
  191. output += '${MathUtils.rationalToString(p)} 百分位数: ${MathUtils.rationalToString(percentile)} (近似值: ${percentile.toDouble()})';
  192. break;
  193. }
  194. cumulative += frequencies[i];
  195. }
  196. }
  197. // 生成直方图数据
  198. _generateHistogramData(a, d, frequencies);
  199. setState(() {
  200. result = output;
  201. });
  202. } catch (e) {
  203. setState(() {
  204. result = '错误: $e';
  205. barGroups = null;
  206. groupLabels = null;
  207. });
  208. }
  209. }
  210. void _generateHistogramData(Rational a, Rational d, List<Rational> frequencies) {
  211. barGroups = [];
  212. groupLabels = [];
  213. maxFrequencyDensity = 0.0;
  214. for (int i = 0; i < frequencies.length; i++) {
  215. final left = a + MathUtils.fromInt(i) * d;
  216. final right = left + d;
  217. // 计算频率/组距作为纵坐标
  218. final frequencyDensity = (frequencies[i] / d).toDouble();
  219. // 更新最大频率密度
  220. if (frequencyDensity > maxFrequencyDensity!) {
  221. maxFrequencyDensity = frequencyDensity;
  222. }
  223. barGroups!.add(
  224. BarChartGroupData(
  225. x: i,
  226. barRods: [
  227. BarChartRodData(
  228. toY: frequencyDensity,
  229. width: 16,
  230. color: Colors.blue,
  231. borderRadius: BorderRadius.zero,
  232. ),
  233. ],
  234. ),
  235. );
  236. groupLabels!.add(
  237. '[${MathUtils.rationalToString(left)}, ${MathUtils.rationalToString(right)})',
  238. );
  239. }
  240. // 确保maxFrequencyDensity不为0
  241. if (maxFrequencyDensity == 0.0) {
  242. maxFrequencyDensity = 1.0;
  243. }
  244. }
  245. @override
  246. void dispose() {
  247. leftEndpointController.dispose();
  248. groupDistanceController.dispose();
  249. frequenciesController.dispose();
  250. percentileController.dispose();
  251. super.dispose();
  252. }
  253. }