// 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 { final TextEditingController leftEndpointController = TextEditingController(); final TextEditingController groupDistanceController = TextEditingController(); final TextEditingController frequenciesController = TextEditingController(); final TextEditingController percentileController = TextEditingController(); String result = ''; List? barGroups; List? 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 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(); } }