| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449 |
- // tools/function_plotter.dart
- import 'package:flutter/material.dart';
- import 'package:math_expressions/math_expressions.dart';
- import 'package:fl_chart/fl_chart.dart';
- class FunctionPlotterPage extends StatefulWidget {
- @override
- _FunctionPlotterPageState createState() => _FunctionPlotterPageState();
- }
- class _FunctionPlotterPageState extends State<FunctionPlotterPage> {
- final List<TextEditingController> _functionControllers = [
- TextEditingController(text: 'x^2'),
- ];
- final TextEditingController _xMinController = TextEditingController(text: '-5');
- final TextEditingController _xMaxController = TextEditingController(text: '5');
- final TextEditingController _yMinController = TextEditingController(text: '-10');
- final TextEditingController _yMaxController = TextEditingController(text: '10');
-
- List<Color> _functionColors = [
- Colors.blue,
- Colors.red,
- Colors.green,
- Colors.orange,
- Colors.purple,
- ];
-
- List<String> _functionErrors = [];
- @override
- void dispose() {
- for (var controller in _functionControllers) {
- controller.dispose();
- }
- _xMinController.dispose();
- _xMaxController.dispose();
- _yMinController.dispose();
- _yMaxController.dispose();
- super.dispose();
- }
- void _addFunction() {
- if (_functionControllers.length < 5) {
- setState(() {
- _functionControllers.add(TextEditingController());
- });
- }
- }
- void _removeFunction(int index) {
- if (_functionControllers.length > 1) {
- setState(() {
- _functionControllers.removeAt(index);
- if (_functionErrors.length > index) {
- _functionErrors.removeAt(index);
- }
- });
- }
- }
- List<FlSpot> _evaluateFunction(String function, double xMin, double xMax, int functionIndex) {
- List<FlSpot> points = [];
-
- try {
- Parser p = Parser();
- Expression exp = p.parse(function);
- ContextModel cm = ContextModel();
-
- int numberOfPoints = 200;
- double step = (xMax - xMin) / numberOfPoints;
-
- for (int i = 0; i <= numberOfPoints; i++) {
- double xValue = xMin + i * step;
-
- try {
- cm.bindVariable(Variable('x'), Number(xValue));
- double yValue = exp.evaluate(EvaluationType.REAL, cm);
-
- if (yValue.isFinite && yValue.abs() < 1e10) {
- points.add(FlSpot(xValue, yValue));
- } else {
- points.add(FlSpot.nullSpot);
- }
- } catch (e) {
- points.add(FlSpot.nullSpot);
- }
- }
- } catch (e) {
- if (_functionErrors.length <= functionIndex) {
- _functionErrors.addAll(List.filled(functionIndex - _functionErrors.length + 1, ''));
- }
- _functionErrors[functionIndex] = '函数解析错误: ${e.toString()}';
- }
-
- return points;
- }
- void _plotFunctions() {
- setState(() {
- _functionErrors.clear();
- });
- }
- @override
- Widget build(BuildContext context) {
- return Scaffold(
- appBar: AppBar(
- title: Text('函数图像绘制', style: TextStyle(color: Colors.black)),
- backgroundColor: Colors.white,
- elevation: 1,
- actions: [
- IconButton(
- icon: Icon(Icons.refresh),
- onPressed: _plotFunctions,
- tooltip: '重新绘制',
- ),
- ],
- ),
- body: SingleChildScrollView( // 添加外层滚动
- child: Padding(
- padding: EdgeInsets.all(16),
- child: Column(
- children: [
- // 函数输入区域 - 移除固定高度约束
- _buildFunctionInputs(),
-
- SizedBox(height: 16), // 添加间距
-
- // 绘图区域 - 设置固定高度
- Container(
- height: MediaQuery.of(context).size.height * 0.5, // 使用屏幕高度的一半
- child: _buildPlotArea(),
- ),
- ],
- ),
- ),
- ),
- );
- }
- Widget _buildFunctionInputs() {
- return Card(
- elevation: 4,
- child: Padding(
- padding: EdgeInsets.all(16),
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- Text(
- '函数表达式 (使用 x 作为变量)',
- style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
- ),
- SizedBox(height: 10),
- Text(
- '支持: +, -, *, /, ^, sin, cos, tan, ln, sqrt, e',
- style: TextStyle(fontSize: 12, color: Colors.grey),
- ),
- SizedBox(height: 10),
-
- // 坐标范围设置 - 在小屏幕上改为垂直布局
- LayoutBuilder(
- builder: (context, constraints) {
- if (constraints.maxWidth < 600) {
- // 小屏幕垂直布局
- return Column(
- children: [
- _buildRangeTextField(_xMinController, 'x最小值'),
- SizedBox(height: 10),
- _buildRangeTextField(_xMaxController, 'x最大值'),
- SizedBox(height: 10),
- _buildRangeTextField(_yMinController, 'y最小值'),
- SizedBox(height: 10),
- _buildRangeTextField(_yMaxController, 'y最大值'),
- ],
- );
- } else {
- // 大屏幕水平布局
- return Row(
- children: [
- Expanded(child: _buildRangeTextField(_xMinController, 'x最小值')),
- SizedBox(width: 10),
- Expanded(child: _buildRangeTextField(_xMaxController, 'x最大值')),
- SizedBox(width: 10),
- Expanded(child: _buildRangeTextField(_yMinController, 'y最小值')),
- SizedBox(width: 10),
- Expanded(child: _buildRangeTextField(_yMaxController, 'y最大值')),
- ],
- );
- }
- },
- ),
- SizedBox(height: 10),
-
- // 函数输入列表 - 设置最大高度并允许滚动
- Container(
- constraints: BoxConstraints(
- maxHeight: 200, // 限制最大高度
- ),
- child: ListView.builder(
- shrinkWrap: true,
- itemCount: _functionControllers.length,
- itemBuilder: (context, index) {
- return Padding(
- padding: EdgeInsets.symmetric(vertical: 5),
- child: Row(
- children: [
- Container(
- width: 20,
- height: 20,
- decoration: BoxDecoration(
- color: _functionColors[index],
- shape: BoxShape.circle,
- ),
- ),
- SizedBox(width: 10),
- Expanded(
- child: TextField(
- controller: _functionControllers[index],
- decoration: InputDecoration(
- hintText: '例如: x^2, sin(x), ln(x)',
- border: OutlineInputBorder(),
- errorText: _functionErrors.length > index ? _functionErrors[index] : null,
- ),
- onChanged: (value) => _plotFunctions(),
- ),
- ),
- SizedBox(width: 10),
- if (_functionControllers.length > 1)
- IconButton(
- icon: Icon(Icons.remove, color: Colors.red),
- onPressed: () => _removeFunction(index),
- ),
- ],
- ),
- );
- },
- ),
- ),
-
- SizedBox(height: 10),
-
- // 添加函数按钮
- if (_functionControllers.length < 5)
- Center(
- child: ElevatedButton.icon(
- onPressed: () {
- _addFunction();
- _plotFunctions();
- },
- icon: Icon(Icons.add),
- label: Text('添加函数'),
- style: ElevatedButton.styleFrom(
- backgroundColor: Colors.blue,
- foregroundColor: Colors.white,
- ),
- ),
- ),
- ],
- ),
- ),
- );
- }
- // 辅助方法:构建范围输入框
- Widget _buildRangeTextField(TextEditingController controller, String label) {
- return TextField(
- controller: controller,
- decoration: InputDecoration(
- labelText: label,
- border: OutlineInputBorder(),
- contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 8),
- ),
- keyboardType: TextInputType.numberWithOptions(signed: true, decimal: true),
- onChanged: (value) => _plotFunctions(),
- );
- }
- Widget _buildPlotArea() {
- return Card(
- elevation: 4,
- child: Padding(
- padding: EdgeInsets.all(16),
- child: Column(
- children: [
- Text(
- '函数图像',
- style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
- ),
- SizedBox(height: 10),
- Expanded(
- child: _buildPlot(),
- ),
- ],
- ),
- ),
- );
- }
- Widget _buildPlot() {
- double xMin, xMax, yMin, yMax;
-
- try {
- xMin = double.parse(_xMinController.text);
- xMax = double.parse(_xMaxController.text);
- yMin = double.parse(_yMinController.text);
- yMax = double.parse(_yMaxController.text);
- } catch (e) {
- return Center(
- child: Text('请输入有效的坐标范围', style: TextStyle(color: Colors.red)),
- );
- }
-
- if (xMin >= xMax || yMin >= yMax) {
- return Center(
- child: Text('最小值必须小于最大值', style: TextStyle(color: Colors.red)),
- );
- }
-
- List<LineChartBarData> lineBars = [];
-
- for (int i = 0; i < _functionControllers.length; i++) {
- String function = _functionControllers[i].text.trim();
- if (function.isEmpty) continue;
-
- List<FlSpot> spots = _evaluateFunction(function, xMin, xMax, i);
-
- if (spots.isNotEmpty) {
- lineBars.add(
- LineChartBarData(
- spots: spots,
- isCurved: true,
- color: _functionColors[i],
- barWidth: 2,
- isStrokeCapRound: true,
- dotData: FlDotData(show: false),
- belowBarData: BarAreaData(show: false),
- ),
- );
- }
- }
-
- if (lineBars.isEmpty) {
- return Center(
- child: Column(
- mainAxisAlignment: MainAxisAlignment.center,
- children: [
- Icon(Icons.functions, size: 50, color: Colors.grey),
- SizedBox(height: 10),
- Text('暂无函数图像可显示', style: TextStyle(color: Colors.grey)),
- Text('请输入有效的函数表达式', style: TextStyle(color: Colors.grey, fontSize: 12)),
- ],
- ),
- );
- }
-
- return LineChart(
- LineChartData(
- clipData: FlClipData.all(),
- gridData: FlGridData(
- show: true,
- drawVerticalLine: true,
- drawHorizontalLine: true,
- getDrawingHorizontalLine: (value) {
- return FlLine(
- color: Colors.grey.withOpacity(0.3),
- strokeWidth: 1,
- );
- },
- getDrawingVerticalLine: (value) {
- return FlLine(
- color: Colors.grey.withOpacity(0.3),
- strokeWidth: 1,
- );
- },
- ),
- titlesData: FlTitlesData(
- show: true,
- bottomTitles: AxisTitles(
- sideTitles: SideTitles(
- showTitles: true,
- reservedSize: 30,
- interval: _calculateInterval(xMin, xMax),
- getTitlesWidget: (value, meta) {
- return Text(
- value.toStringAsFixed(1),
- style: TextStyle(fontSize: 10, color: Colors.black),
- );
- },
- ),
- ),
- leftTitles: AxisTitles(
- sideTitles: SideTitles(
- showTitles: true,
- reservedSize: 40,
- interval: _calculateInterval(yMin, yMax),
- getTitlesWidget: (value, meta) {
- return Text(
- value.toStringAsFixed(1),
- style: TextStyle(fontSize: 10, color: Colors.black),
- );
- },
- ),
- ),
- rightTitles: AxisTitles(sideTitles: SideTitles(showTitles: false)),
- topTitles: AxisTitles(sideTitles: SideTitles(showTitles: false)),
- ),
- borderData: FlBorderData(
- show: true,
- border: Border.all(color: Colors.black, width: 2),
- ),
- minX: xMin,
- maxX: xMax,
- minY: yMin,
- maxY: yMax,
- lineBarsData: lineBars,
- lineTouchData: LineTouchData(enabled: false),
- extraLinesData: ExtraLinesData(
- horizontalLines: [
- HorizontalLine(
- y: 0,
- color: Colors.black,
- strokeWidth: 1,
- ),
- ],
- verticalLines: [
- VerticalLine(
- x: 0,
- color: Colors.black,
- strokeWidth: 1,
- ),
- ],
- ),
- ),
- );
- }
- double _calculateInterval(double min, double max) {
- double range = max - min;
- if (range <= 0) return 1.0;
-
- if (range <= 2) return 0.5;
- if (range <= 5) return 1.0;
- if (range <= 10) return 2.0;
- if (range <= 20) return 5.0;
- if (range <= 50) return 10.0;
- if (range <= 100) return 20.0;
- return 50.0;
- }
- }
|