| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170 |
- // tools/chi_square.dart
- import 'package:fl_chart/fl_chart.dart';
- import 'package:flutter/material.dart';
- import 'tool_kit.dart';
- /// 卡方检验(2×2 列联表)。
- ///
- /// K² = n(ad − bc)² / ((a+b)(c+d)(a+c)(b+d)),n = a+b+c+d
- ///
- /// 精确值给分数、近似值给小数,都从 Symja 拿。图表里的比例数据同样走 Symja
- /// (`N[...]`),Dart 只负责把数字喂给 fl_chart —— 图表不参与复制。
- class ChiSquarePage extends StatefulWidget {
- const ChiSquarePage({super.key});
- @override
- State<ChiSquarePage> createState() => _ChiSquarePageState();
- }
- class _ChiSquarePageState extends State<ChiSquarePage> {
- // 2×2 列联表就照列联表的样子排:a b 一行、c d 一行
- final _a = ToolField('a', hint: '第一行左');
- final _b = ToolField('b', hint: '第一行右', rowWithPrevious: true);
- final _c = ToolField('c', hint: '第二行左');
- final _d = ToolField('d', hint: '第二行右', rowWithPrevious: true);
- @override
- Widget build(BuildContext context) => ToolScaffold(
- title: '卡方检验',
- fields: [_a, _b, _c, _d],
- intro: const Text('请输入 2×2 列联表数据:'),
- compute: (session) async {
- final a = _need(_a);
- final b = _need(_b);
- final c = _need(_c);
- final d = _need(_d);
- final bind = 'a=$a,b=$b,c=$c,d=$d';
- final k2 =
- 'With[{$bind}, Module[{nn=a+b+c+d}, '
- 'nn*(a*d-b*c)^2/((a+b)*(c+d)*(a+c)*(b+d))]]';
- // 各组比例(给图表,不复制):组1 = a/(a+b)、b/(a+b),组2 = c/(c+d)、d/(c+d)
- final prop = 'With[{$bind}, N[{a/(a+b), b/(a+b), c/(c+d), d/(c+d)}]]';
- final p = parseSymjaList((await session.compute(prop)).text);
- final n0 = p.isNotEmpty ? asNumber(p[0]) ?? 0.0 : 0.0;
- final n1 = p.length > 1 ? asNumber(p[1]) ?? 0.0 : 0.0;
- final n2 = p.length > 2 ? asNumber(p[2]) ?? 0.0 : 0.0;
- final n3 = p.length > 3 ? asNumber(p[3]) ?? 0.0 : 0.0;
- return [
- ...await exactAndApprox(session, 'K²', k2),
- ToolChart('各组比例', _barChart(n0, n1, n2, n3)),
- ];
- },
- );
- /// 等高堆积条形图:两组各按 类别A / 类别B 的比例堆积,纵轴百分比。
- Widget _barChart(double a1, double b1, double a2, double b2) => Column(
- mainAxisSize: MainAxisSize.min,
- children: [
- SizedBox(
- height: 220,
- child: BarChart(
- BarChartData(
- alignment: BarChartAlignment.spaceAround,
- maxY: 1.0,
- barTouchData: BarTouchData(enabled: false),
- titlesData: FlTitlesData(
- show: true,
- bottomTitles: AxisTitles(
- sideTitles: SideTitles(
- showTitles: true,
- getTitlesWidget: (value, meta) =>
- Text(value.toInt() == 0 ? '组1' : '组2'),
- ),
- ),
- leftTitles: AxisTitles(
- sideTitles: SideTitles(
- showTitles: true,
- reservedSize: 40,
- getTitlesWidget: (value, meta) =>
- Text('${(value * 100).toInt()}%'),
- ),
- ),
- topTitles: AxisTitles(sideTitles: SideTitles(showTitles: false)),
- rightTitles: AxisTitles(
- sideTitles: SideTitles(showTitles: false),
- ),
- ),
- gridData: FlGridData(
- show: true,
- drawVerticalLine: false,
- getDrawingHorizontalLine: (value) => FlLine(
- color: Colors.grey.withValues(alpha: 0.3),
- strokeWidth: 1,
- ),
- ),
- borderData: FlBorderData(show: false),
- barGroups: [
- BarChartGroupData(
- x: 0,
- groupVertically: true,
- barsSpace: 4,
- barRods: [
- BarChartRodData(
- fromY: 0,
- toY: a1,
- color: Colors.blue,
- width: 30,
- ),
- BarChartRodData(
- fromY: a1,
- toY: a1 + b1,
- color: Colors.red,
- width: 30,
- ),
- ],
- ),
- BarChartGroupData(
- x: 1,
- groupVertically: true,
- barsSpace: 4,
- barRods: [
- BarChartRodData(
- fromY: 0,
- toY: a2,
- color: Colors.blue,
- width: 30,
- ),
- BarChartRodData(
- fromY: a2,
- toY: a2 + b2,
- color: Colors.red,
- width: 30,
- ),
- ],
- ),
- ],
- ),
- ),
- ),
- const SizedBox(height: 10),
- Row(
- mainAxisAlignment: MainAxisAlignment.center,
- children: [
- _legend(Colors.blue, '类别A'),
- const SizedBox(width: 20),
- _legend(Colors.red, '类别B'),
- ],
- ),
- ],
- );
- Widget _legend(Color color, String text) => Row(
- mainAxisSize: MainAxisSize.min,
- children: [
- Container(width: 16, height: 16, color: color),
- const SizedBox(width: 8),
- Text(text),
- ],
- );
- /// 取一个标量输入框:空着或填了多个值都直接报中文错。
- String _need(ToolField f) {
- if (f.count == 0) throw Exception('请填写「${f.label}」');
- if (f.count > 1) throw Exception('「${f.label}」只能填一个数');
- return f.text;
- }
- }
|