chi_square.dart 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171
  1. // tools/chi_square.dart
  2. import 'package:fl_chart/fl_chart.dart';
  3. import 'package:flutter/material.dart';
  4. import 'tool_kit.dart';
  5. /// 卡方检验(2×2 列联表)。
  6. ///
  7. /// K² = n(ad − bc)² / ((a+b)(c+d)(a+c)(b+d)),n = a+b+c+d
  8. ///
  9. /// 精确值给分数、近似值给小数,都从 Symja 拿。图表里的比例数据同样走 Symja
  10. /// (`N[...]`),Dart 只负责把数字喂给 fl_chart —— 图表不参与复制。
  11. class ChiSquarePage extends StatefulWidget {
  12. const ChiSquarePage({super.key});
  13. @override
  14. State<ChiSquarePage> createState() => _ChiSquarePageState();
  15. }
  16. class _ChiSquarePageState extends State<ChiSquarePage> {
  17. final _a = ToolField('a', hint: '第一行左');
  18. final _b = ToolField('b', hint: '第一行右');
  19. final _c = ToolField('c', hint: '第二行左');
  20. final _d = ToolField('d', hint: '第二行右');
  21. @override
  22. Widget build(BuildContext context) => ToolScaffold(
  23. title: '卡方检验',
  24. fields: [_a, _b, _c, _d],
  25. intro: const Text('请输入 2×2 列联表数据:'),
  26. compute: (session) async {
  27. final a = _need(_a);
  28. final b = _need(_b);
  29. final c = _need(_c);
  30. final d = _need(_d);
  31. final bind = 'a=$a,b=$b,c=$c,d=$d';
  32. final k2 = 'With[{$bind}, Module[{nn=a+b+c+d}, '
  33. 'nn*(a*d-b*c)^2/((a+b)*(c+d)*(a+c)*(b+d))]]';
  34. // 各组比例(给图表,不复制):组1 = a/(a+b)、b/(a+b),组2 = c/(c+d)、d/(c+d)
  35. final prop =
  36. 'With[{$bind}, N[{a/(a+b), b/(a+b), c/(c+d), d/(c+d)}]]';
  37. final p = parseSymjaList((await session.compute(prop)).text);
  38. final n0 = p.isNotEmpty ? asNumber(p[0]) ?? 0.0 : 0.0;
  39. final n1 = p.length > 1 ? asNumber(p[1]) ?? 0.0 : 0.0;
  40. final n2 = p.length > 2 ? asNumber(p[2]) ?? 0.0 : 0.0;
  41. final n3 = p.length > 3 ? asNumber(p[3]) ?? 0.0 : 0.0;
  42. return [
  43. ...await exactAndApprox(session, 'K²', k2),
  44. ToolChart('各组比例', _barChart(n0, n1, n2, n3)),
  45. ];
  46. },
  47. );
  48. /// 等高堆积条形图:两组各按 类别A / 类别B 的比例堆积,纵轴百分比。
  49. Widget _barChart(double a1, double b1, double a2, double b2) => Column(
  50. mainAxisSize: MainAxisSize.min,
  51. children: [
  52. SizedBox(
  53. height: 220,
  54. child: BarChart(
  55. BarChartData(
  56. alignment: BarChartAlignment.spaceAround,
  57. maxY: 1.0,
  58. barTouchData: BarTouchData(enabled: false),
  59. titlesData: FlTitlesData(
  60. show: true,
  61. bottomTitles: AxisTitles(
  62. sideTitles: SideTitles(
  63. showTitles: true,
  64. getTitlesWidget: (value, meta) =>
  65. Text(value.toInt() == 0 ? '组1' : '组2'),
  66. ),
  67. ),
  68. leftTitles: AxisTitles(
  69. sideTitles: SideTitles(
  70. showTitles: true,
  71. reservedSize: 40,
  72. getTitlesWidget: (value, meta) =>
  73. Text('${(value * 100).toInt()}%'),
  74. ),
  75. ),
  76. topTitles: AxisTitles(
  77. sideTitles: SideTitles(showTitles: false),
  78. ),
  79. rightTitles: AxisTitles(
  80. sideTitles: SideTitles(showTitles: false),
  81. ),
  82. ),
  83. gridData: FlGridData(
  84. show: true,
  85. drawVerticalLine: false,
  86. getDrawingHorizontalLine: (value) => FlLine(
  87. color: Colors.grey.withValues(alpha: 0.3),
  88. strokeWidth: 1,
  89. ),
  90. ),
  91. borderData: FlBorderData(show: false),
  92. barGroups: [
  93. BarChartGroupData(
  94. x: 0,
  95. groupVertically: true,
  96. barsSpace: 4,
  97. barRods: [
  98. BarChartRodData(
  99. fromY: 0,
  100. toY: a1,
  101. color: Colors.blue,
  102. width: 30,
  103. ),
  104. BarChartRodData(
  105. fromY: a1,
  106. toY: a1 + b1,
  107. color: Colors.red,
  108. width: 30,
  109. ),
  110. ],
  111. ),
  112. BarChartGroupData(
  113. x: 1,
  114. groupVertically: true,
  115. barsSpace: 4,
  116. barRods: [
  117. BarChartRodData(
  118. fromY: 0,
  119. toY: a2,
  120. color: Colors.blue,
  121. width: 30,
  122. ),
  123. BarChartRodData(
  124. fromY: a2,
  125. toY: a2 + b2,
  126. color: Colors.red,
  127. width: 30,
  128. ),
  129. ],
  130. ),
  131. ],
  132. ),
  133. ),
  134. ),
  135. const SizedBox(height: 10),
  136. Row(
  137. mainAxisAlignment: MainAxisAlignment.center,
  138. children: [
  139. _legend(Colors.blue, '类别A'),
  140. const SizedBox(width: 20),
  141. _legend(Colors.red, '类别B'),
  142. ],
  143. ),
  144. ],
  145. );
  146. Widget _legend(Color color, String text) => Row(
  147. mainAxisSize: MainAxisSize.min,
  148. children: [
  149. Container(width: 16, height: 16, color: color),
  150. const SizedBox(width: 8),
  151. Text(text),
  152. ],
  153. );
  154. /// 取一个标量输入框:空着或填了多个值都直接报中文错。
  155. String _need(ToolField f) {
  156. if (f.count == 0) throw Exception('请填写「${f.label}」');
  157. if (f.count > 1) throw Exception('「${f.label}」只能填一个数');
  158. return f.text;
  159. }
  160. }