| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164 |
- // tools/histogram.dart
- import 'package:fl_chart/fl_chart.dart';
- import 'package:flutter/material.dart';
- import 'tool_kit.dart';
- /// 直方图统计。
- ///
- /// 组中值 midᵢ = a + i·d + d/2(i 从 0 起)
- /// 均值 = Σ midᵢfᵢ
- /// 方差 = Σ (midᵢ-均值)²fᵢ
- /// 中位数 / 百分位数:累计频率首次超过目标 p 的那一组内插
- ///
- /// 频率和、均值、方差、分位数全部交给 Symja 求精确值;图表要 double,
- /// 才另跑一份 `N[...]` 的数值版。
- class HistogramPage extends StatefulWidget {
- const HistogramPage({super.key});
- @override
- State<HistogramPage> createState() => _HistogramPageState();
- }
- class _HistogramPageState extends State<HistogramPage> {
- final _a = ToolField('左端点 a', hint: '如 0');
- final _d = ToolField('组距 d', hint: '如 1');
- final _f = ToolField('各组频率(用空格分隔)', hint: '用空格分隔多个值,和为 1');
- final _pct = ToolField('百分位数(可选)', hint: '留空则不算,如 3/4 或 0.75');
- @override
- Widget build(BuildContext context) => ToolScaffold(
- title: '直方图统计',
- fields: [_a, _d, _f, _pct],
- compute: (session) async {
- final a = _a.text, d = _d.text;
- if (a.isEmpty || d.isEmpty) throw Exception('请填写左端点 a 和组距 d');
- if (_f.count == 0) throw Exception('请至少填一个频率');
- final f = _f.list;
- // 频率之和必须为 1:交给 Symja 求 Total,原文不是 1 就报错。
- final sum = await session.compute('Total[$f]');
- if (sum.text != '1') {
- throw Exception('各组频率之和必须为 1,当前为 ${sum.text}');
- }
- // mid 在均值和方差两个式子里都要用,用嵌套 With 定义一次,避免重复一大串。
- final mean = 'With[{a=$a,d=$d,f=$f}, '
- 'With[{mid=Table[a+i*d+d/2,{i,0,Length[f]-1}]}, Total[mid*f]]]';
- final variance = 'With[{a=$a,d=$d,f=$f}, '
- 'With[{mid=Table[a+i*d+d/2,{i,0,Length[f]-1}]}, '
- 'Total[(mid-Total[mid*f])^2*f]]]';
- final outputs = <ToolOutput>[
- ...await exactAndApprox(session, '均值', mean),
- ...await exactAndApprox(session, '方差', variance),
- ...await exactAndApprox(session, '中位数', _quantile(a, d, f, '1/2')),
- ];
- if (_pct.text.isNotEmpty) {
- outputs.addAll(await exactAndApprox(
- session,
- '${_pct.text} 百分位数',
- _quantile(a, d, f, _pct.text),
- ));
- }
- // 图表:横轴标签 [左, 右),左端点/右端点用精确值;柱高是频率密度 f/d,用数值。
- final lr = parseSymjaList((await session.compute(
- 'With[{a=$a,d=$d,f=$f}, Table[{a+i*d, a+(i+1)*d}, {i,0,Length[f]-1}]]',
- ))
- .text);
- final dens = parseSymjaList((await session.compute(
- 'With[{a=$a,d=$d,f=$f}, N[Table[f[[i+1]]/d, {i,0,Length[f]-1}], 8]]',
- ))
- .text)
- .map(asNumber)
- .whereType<double>()
- .toList(growable: false);
- if (lr.length == dens.length && dens.isNotEmpty) {
- outputs.add(ToolChart('直方图', _histChart(lr, dens)));
- }
- return outputs;
- },
- );
- /// 累计频率首次超过 p 的那一组内插。中位数把 p 取 1/2。
- String _quantile(String a, String d, String f, String p) =>
- 'With[{a=$a,d=$d,f=$f,p=$p}, '
- 'Module[{cum=Accumulate[f],k}, '
- 'k=First[FirstPosition[cum, q_ /; q>p]]; '
- 'a+(k-1)*d+(p-If[k==1,0,cum[[k-1]]])/f[[k]]*d]]';
- Widget _histChart(List<Object> lr, List<double> dens) {
- var maxD = 0.0;
- for (final v in dens) {
- if (v > maxD) maxD = v;
- }
- final labels = <String>[
- for (final item in lr)
- if (item is List && item.length == 2)
- '[${item[0]}, ${item[1]})'
- else
- '$item',
- ];
- return SizedBox(
- height: 400,
- child: BarChart(
- BarChartData(
- alignment: BarChartAlignment.spaceBetween,
- groupsSpace: 0,
- maxY: maxD == 0 ? 1.0 : maxD * 1.1,
- barTouchData: BarTouchData(enabled: false),
- borderData: FlBorderData(show: true),
- gridData: FlGridData(show: false),
- titlesData: FlTitlesData(
- bottomTitles: AxisTitles(
- sideTitles: SideTitles(
- showTitles: true,
- reservedSize: 40,
- getTitlesWidget: (value, meta) {
- final i = value.toInt();
- if (i < 0 || i >= labels.length) return const Text('');
- return Padding(
- padding: const EdgeInsets.only(top: 8),
- child: Text(
- labels[i],
- style: const TextStyle(
- fontSize: 10, fontWeight: FontWeight.bold),
- textAlign: TextAlign.center,
- ),
- );
- },
- ),
- ),
- leftTitles: AxisTitles(
- sideTitles: SideTitles(
- showTitles: true,
- reservedSize: 40,
- getTitlesWidget: (value, meta) =>
- Text(value.toStringAsFixed(2)),
- ),
- ),
- topTitles: AxisTitles(sideTitles: SideTitles(showTitles: false)),
- rightTitles: AxisTitles(sideTitles: SideTitles(showTitles: false)),
- ),
- barGroups: [
- for (var i = 0; i < dens.length; i++)
- BarChartGroupData(
- x: i,
- barRods: [
- BarChartRodData(
- toY: dens[i],
- width: 16,
- color: Colors.blue,
- borderRadius: BorderRadius.zero,
- ),
- ],
- ),
- ],
- ),
- ),
- );
- }
- }
|