| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276 |
- // tools/histogram.dart
- import 'package:flutter/material.dart';
- import 'package:rational/rational.dart';
- import '../utils/math_utils.dart';
- import 'package:fl_chart/fl_chart.dart';
- class HistogramPage extends StatefulWidget {
- @override
- _HistogramPageState createState() => _HistogramPageState();
- }
- class _HistogramPageState extends State<HistogramPage> {
- final TextEditingController leftEndpointController = TextEditingController();
- final TextEditingController groupDistanceController = TextEditingController();
- final TextEditingController frequenciesController = TextEditingController();
- final TextEditingController percentileController = TextEditingController();
- String result = '';
- List<BarChartGroupData>? barGroups;
- List<String>? groupLabels;
- double? maxFrequencyDensity;
- @override
- Widget build(BuildContext context) {
- return Scaffold(
- appBar: AppBar(title: Text('直方图统计')),
- body: SingleChildScrollView( // 添加滚动视图
- padding: EdgeInsets.all(16),
- child: Column(
- children: [
- _buildInputField('左端点', leftEndpointController),
- _buildInputField('组距', groupDistanceController),
- _buildInputField('各组频率(用空格分隔)', frequenciesController),
- _buildInputField('百分位数(可选)', percentileController),
- SizedBox(height: 20),
- ElevatedButton(
- onPressed: _calculate,
- child: Text('计算'),
- ),
- SizedBox(height: 20),
- if (result.isNotEmpty)
- Card(
- child: Padding(
- padding: EdgeInsets.all(16),
- child: Text(
- result,
- style: TextStyle(fontSize: 14, fontFamily: 'Monospace'),
- ),
- ),
- ),
- if (barGroups != null && barGroups!.isNotEmpty)
- SizedBox( // 将 Expanded 改为 SizedBox 并指定高度
- height: 400, // 设置固定高度
- child: Card(
- child: Padding(
- padding: EdgeInsets.all(16),
- child: Column(
- children: [
- Text(
- '直方图',
- style: TextStyle(
- fontSize: 18,
- fontWeight: FontWeight.bold,
- ),
- ),
- SizedBox(height: 10),
- Expanded( // 这个 Expanded 可以保留,因为它是在 Column 内部
- child: BarChart(
- BarChartData(
- barGroups: barGroups,
- alignment: BarChartAlignment.spaceBetween,
- groupsSpace: 0,
- barTouchData: BarTouchData(
- enabled: false,
- ),
- borderData: FlBorderData(show: true),
- gridData: FlGridData(show: false),
- titlesData: FlTitlesData(
- bottomTitles: AxisTitles(
- sideTitles: SideTitles(
- showTitles: true,
- getTitlesWidget: (value, meta) {
- final index = value.toInt();
- if (index >= 0 && index < (groupLabels?.length ?? 0)) {
- return Padding(
- padding: const EdgeInsets.only(top: 8.0),
- child: Text(
- groupLabels![index],
- style: TextStyle(
- fontSize: 10,
- fontWeight: FontWeight.bold,
- ),
- textAlign: TextAlign.center,
- ),
- );
- }
- return Text('');
- },
- reservedSize: 40,
- ),
- ),
- leftTitles: AxisTitles(
- sideTitles: SideTitles(
- showTitles: true,
- reservedSize: 40,
- getTitlesWidget: (value, meta) {
- return Text(value.toStringAsFixed(2));
- },
- ),
- ),
- rightTitles: AxisTitles(
- sideTitles: SideTitles(showTitles: false),
- ),
- topTitles: AxisTitles(
- sideTitles: SideTitles(showTitles: false),
- ),
- ),
- maxY: maxFrequencyDensity != null
- ? (maxFrequencyDensity! * 1.1)
- : 1.0,
- ),
- ),
- ),
- ],
- ),
- ),
- ),
- ),
- ],
- ),
- ),
- );
- }
- Widget _buildInputField(String label, TextEditingController controller) {
- return Padding(
- padding: EdgeInsets.symmetric(vertical: 8),
- child: TextField(
- controller: controller,
- keyboardType: TextInputType.numberWithOptions(signed: true, decimal: true),
- decoration: InputDecoration(
- labelText: label,
- border: OutlineInputBorder(),
- hintText: label.contains('频率') ? '用空格分隔多个值' : '输入分数如: 1/2 或小数',
- ),
- ),
- );
- }
- void _calculate() {
- try {
- final a = MathUtils.parseRational(leftEndpointController.text);
- final d = MathUtils.parseRational(groupDistanceController.text);
- final frequencies = frequenciesController.text.split(' ').map((s) => MathUtils.parseRational(s)).toList();
- Rational sumFreq = MathUtils.zero;
- for (final f in frequencies) {
- sumFreq += f;
- }
- if (sumFreq != MathUtils.one) {
- setState(() {
- result = '错误: 频率之和必须为1,当前为 ${MathUtils.rationalToString(sumFreq)}';
- barGroups = null;
- groupLabels = null;
- });
- return;
- }
- // 计算均值
- Rational mean = MathUtils.zero;
- for (int i = 0; i < frequencies.length; i++) {
- mean += (a + MathUtils.fromInt(i) * d + d / MathUtils.two) * frequencies[i];
- }
- // 计算方差
- Rational variance = MathUtils.zero;
- for (int i = 0; i < frequencies.length; i++) {
- final midPoint = a + MathUtils.fromInt(i) * d + d / MathUtils.two;
- variance += (midPoint - mean).pow(2) * frequencies[i];
- }
- String output = '均值: ${MathUtils.rationalToString(mean)} (近似值: ${mean.toDouble()})\n'
- '方差: ${MathUtils.rationalToString(variance)} (近似值: ${variance.toDouble()})\n';
- // 计算中位数
- Rational cumulative = MathUtils.zero;
- for (int i = 0; i < frequencies.length; i++) {
- if (cumulative + frequencies[i] > MathUtils.half) {
- final median = a + MathUtils.fromInt(i) * d - (cumulative - MathUtils.half) / frequencies[i] * d;
- output += '中位数: ${MathUtils.rationalToString(median)} (近似值: ${median.toDouble()})\n';
- break;
- }
- cumulative += frequencies[i];
- }
- // 计算百分位数
- if (percentileController.text.isNotEmpty) {
- final p = MathUtils.parseRational(percentileController.text);
- cumulative = MathUtils.zero;
- for (int i = 0; i < frequencies.length; i++) {
- if (cumulative + frequencies[i] > p) {
- final percentile = a + MathUtils.fromInt(i) * d - (cumulative - p) / frequencies[i] * d;
- output += '${MathUtils.rationalToString(p)} 百分位数: ${MathUtils.rationalToString(percentile)} (近似值: ${percentile.toDouble()})';
- break;
- }
- cumulative += frequencies[i];
- }
- }
- // 生成直方图数据
- _generateHistogramData(a, d, frequencies);
- setState(() {
- result = output;
- });
- } catch (e) {
- setState(() {
- result = '错误: $e';
- barGroups = null;
- groupLabels = null;
- });
- }
- }
- void _generateHistogramData(Rational a, Rational d, List<Rational> frequencies) {
- barGroups = [];
- groupLabels = [];
- maxFrequencyDensity = 0.0;
- for (int i = 0; i < frequencies.length; i++) {
- final left = a + MathUtils.fromInt(i) * d;
- final right = left + d;
-
- // 计算频率/组距作为纵坐标
- final frequencyDensity = (frequencies[i] / d).toDouble();
-
- // 更新最大频率密度
- if (frequencyDensity > maxFrequencyDensity!) {
- maxFrequencyDensity = frequencyDensity;
- }
- barGroups!.add(
- BarChartGroupData(
- x: i,
- barRods: [
- BarChartRodData(
- toY: frequencyDensity,
- width: 16,
- color: Colors.blue,
- borderRadius: BorderRadius.zero,
- ),
- ],
- ),
- );
- groupLabels!.add(
- '[${MathUtils.rationalToString(left)}, ${MathUtils.rationalToString(right)})',
- );
- }
- // 确保maxFrequencyDensity不为0
- if (maxFrequencyDensity == 0.0) {
- maxFrequencyDensity = 1.0;
- }
- }
- @override
- void dispose() {
- leftEndpointController.dispose();
- groupDistanceController.dispose();
- frequenciesController.dispose();
- percentileController.dispose();
- super.dispose();
- }
- }
|