function_plotter.dart 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449
  1. // tools/function_plotter.dart
  2. import 'package:flutter/material.dart';
  3. import 'package:math_expressions/math_expressions.dart';
  4. import 'package:fl_chart/fl_chart.dart';
  5. class FunctionPlotterPage extends StatefulWidget {
  6. @override
  7. _FunctionPlotterPageState createState() => _FunctionPlotterPageState();
  8. }
  9. class _FunctionPlotterPageState extends State<FunctionPlotterPage> {
  10. final List<TextEditingController> _functionControllers = [
  11. TextEditingController(text: 'x^2'),
  12. ];
  13. final TextEditingController _xMinController = TextEditingController(text: '-5');
  14. final TextEditingController _xMaxController = TextEditingController(text: '5');
  15. final TextEditingController _yMinController = TextEditingController(text: '-10');
  16. final TextEditingController _yMaxController = TextEditingController(text: '10');
  17. List<Color> _functionColors = [
  18. Colors.blue,
  19. Colors.red,
  20. Colors.green,
  21. Colors.orange,
  22. Colors.purple,
  23. ];
  24. List<String> _functionErrors = [];
  25. @override
  26. void dispose() {
  27. for (var controller in _functionControllers) {
  28. controller.dispose();
  29. }
  30. _xMinController.dispose();
  31. _xMaxController.dispose();
  32. _yMinController.dispose();
  33. _yMaxController.dispose();
  34. super.dispose();
  35. }
  36. void _addFunction() {
  37. if (_functionControllers.length < 5) {
  38. setState(() {
  39. _functionControllers.add(TextEditingController());
  40. });
  41. }
  42. }
  43. void _removeFunction(int index) {
  44. if (_functionControllers.length > 1) {
  45. setState(() {
  46. _functionControllers.removeAt(index);
  47. if (_functionErrors.length > index) {
  48. _functionErrors.removeAt(index);
  49. }
  50. });
  51. }
  52. }
  53. List<FlSpot> _evaluateFunction(String function, double xMin, double xMax, int functionIndex) {
  54. List<FlSpot> points = [];
  55. try {
  56. Parser p = Parser();
  57. Expression exp = p.parse(function);
  58. ContextModel cm = ContextModel();
  59. int numberOfPoints = 200;
  60. double step = (xMax - xMin) / numberOfPoints;
  61. for (int i = 0; i <= numberOfPoints; i++) {
  62. double xValue = xMin + i * step;
  63. try {
  64. cm.bindVariable(Variable('x'), Number(xValue));
  65. double yValue = exp.evaluate(EvaluationType.REAL, cm);
  66. if (yValue.isFinite && yValue.abs() < 1e10) {
  67. points.add(FlSpot(xValue, yValue));
  68. } else {
  69. points.add(FlSpot.nullSpot);
  70. }
  71. } catch (e) {
  72. points.add(FlSpot.nullSpot);
  73. }
  74. }
  75. } catch (e) {
  76. if (_functionErrors.length <= functionIndex) {
  77. _functionErrors.addAll(List.filled(functionIndex - _functionErrors.length + 1, ''));
  78. }
  79. _functionErrors[functionIndex] = '函数解析错误: ${e.toString()}';
  80. }
  81. return points;
  82. }
  83. void _plotFunctions() {
  84. setState(() {
  85. _functionErrors.clear();
  86. });
  87. }
  88. @override
  89. Widget build(BuildContext context) {
  90. return Scaffold(
  91. appBar: AppBar(
  92. title: Text('函数图像绘制', style: TextStyle(color: Colors.black)),
  93. backgroundColor: Colors.white,
  94. elevation: 1,
  95. actions: [
  96. IconButton(
  97. icon: Icon(Icons.refresh),
  98. onPressed: _plotFunctions,
  99. tooltip: '重新绘制',
  100. ),
  101. ],
  102. ),
  103. body: SingleChildScrollView( // 添加外层滚动
  104. child: Padding(
  105. padding: EdgeInsets.all(16),
  106. child: Column(
  107. children: [
  108. // 函数输入区域 - 移除固定高度约束
  109. _buildFunctionInputs(),
  110. SizedBox(height: 16), // 添加间距
  111. // 绘图区域 - 设置固定高度
  112. Container(
  113. height: MediaQuery.of(context).size.height * 0.5, // 使用屏幕高度的一半
  114. child: _buildPlotArea(),
  115. ),
  116. ],
  117. ),
  118. ),
  119. ),
  120. );
  121. }
  122. Widget _buildFunctionInputs() {
  123. return Card(
  124. elevation: 4,
  125. child: Padding(
  126. padding: EdgeInsets.all(16),
  127. child: Column(
  128. crossAxisAlignment: CrossAxisAlignment.start,
  129. children: [
  130. Text(
  131. '函数表达式 (使用 x 作为变量)',
  132. style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
  133. ),
  134. SizedBox(height: 10),
  135. Text(
  136. '支持: +, -, *, /, ^, sin, cos, tan, ln, sqrt, e',
  137. style: TextStyle(fontSize: 12, color: Colors.grey),
  138. ),
  139. SizedBox(height: 10),
  140. // 坐标范围设置 - 在小屏幕上改为垂直布局
  141. LayoutBuilder(
  142. builder: (context, constraints) {
  143. if (constraints.maxWidth < 600) {
  144. // 小屏幕垂直布局
  145. return Column(
  146. children: [
  147. _buildRangeTextField(_xMinController, 'x最小值'),
  148. SizedBox(height: 10),
  149. _buildRangeTextField(_xMaxController, 'x最大值'),
  150. SizedBox(height: 10),
  151. _buildRangeTextField(_yMinController, 'y最小值'),
  152. SizedBox(height: 10),
  153. _buildRangeTextField(_yMaxController, 'y最大值'),
  154. ],
  155. );
  156. } else {
  157. // 大屏幕水平布局
  158. return Row(
  159. children: [
  160. Expanded(child: _buildRangeTextField(_xMinController, 'x最小值')),
  161. SizedBox(width: 10),
  162. Expanded(child: _buildRangeTextField(_xMaxController, 'x最大值')),
  163. SizedBox(width: 10),
  164. Expanded(child: _buildRangeTextField(_yMinController, 'y最小值')),
  165. SizedBox(width: 10),
  166. Expanded(child: _buildRangeTextField(_yMaxController, 'y最大值')),
  167. ],
  168. );
  169. }
  170. },
  171. ),
  172. SizedBox(height: 10),
  173. // 函数输入列表 - 设置最大高度并允许滚动
  174. Container(
  175. constraints: BoxConstraints(
  176. maxHeight: 200, // 限制最大高度
  177. ),
  178. child: ListView.builder(
  179. shrinkWrap: true,
  180. itemCount: _functionControllers.length,
  181. itemBuilder: (context, index) {
  182. return Padding(
  183. padding: EdgeInsets.symmetric(vertical: 5),
  184. child: Row(
  185. children: [
  186. Container(
  187. width: 20,
  188. height: 20,
  189. decoration: BoxDecoration(
  190. color: _functionColors[index],
  191. shape: BoxShape.circle,
  192. ),
  193. ),
  194. SizedBox(width: 10),
  195. Expanded(
  196. child: TextField(
  197. controller: _functionControllers[index],
  198. decoration: InputDecoration(
  199. hintText: '例如: x^2, sin(x), ln(x)',
  200. border: OutlineInputBorder(),
  201. errorText: _functionErrors.length > index ? _functionErrors[index] : null,
  202. ),
  203. onChanged: (value) => _plotFunctions(),
  204. ),
  205. ),
  206. SizedBox(width: 10),
  207. if (_functionControllers.length > 1)
  208. IconButton(
  209. icon: Icon(Icons.remove, color: Colors.red),
  210. onPressed: () => _removeFunction(index),
  211. ),
  212. ],
  213. ),
  214. );
  215. },
  216. ),
  217. ),
  218. SizedBox(height: 10),
  219. // 添加函数按钮
  220. if (_functionControllers.length < 5)
  221. Center(
  222. child: ElevatedButton.icon(
  223. onPressed: () {
  224. _addFunction();
  225. _plotFunctions();
  226. },
  227. icon: Icon(Icons.add),
  228. label: Text('添加函数'),
  229. style: ElevatedButton.styleFrom(
  230. backgroundColor: Colors.blue,
  231. foregroundColor: Colors.white,
  232. ),
  233. ),
  234. ),
  235. ],
  236. ),
  237. ),
  238. );
  239. }
  240. // 辅助方法:构建范围输入框
  241. Widget _buildRangeTextField(TextEditingController controller, String label) {
  242. return TextField(
  243. controller: controller,
  244. decoration: InputDecoration(
  245. labelText: label,
  246. border: OutlineInputBorder(),
  247. contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 8),
  248. ),
  249. keyboardType: TextInputType.numberWithOptions(signed: true, decimal: true),
  250. onChanged: (value) => _plotFunctions(),
  251. );
  252. }
  253. Widget _buildPlotArea() {
  254. return Card(
  255. elevation: 4,
  256. child: Padding(
  257. padding: EdgeInsets.all(16),
  258. child: Column(
  259. children: [
  260. Text(
  261. '函数图像',
  262. style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
  263. ),
  264. SizedBox(height: 10),
  265. Expanded(
  266. child: _buildPlot(),
  267. ),
  268. ],
  269. ),
  270. ),
  271. );
  272. }
  273. Widget _buildPlot() {
  274. double xMin, xMax, yMin, yMax;
  275. try {
  276. xMin = double.parse(_xMinController.text);
  277. xMax = double.parse(_xMaxController.text);
  278. yMin = double.parse(_yMinController.text);
  279. yMax = double.parse(_yMaxController.text);
  280. } catch (e) {
  281. return Center(
  282. child: Text('请输入有效的坐标范围', style: TextStyle(color: Colors.red)),
  283. );
  284. }
  285. if (xMin >= xMax || yMin >= yMax) {
  286. return Center(
  287. child: Text('最小值必须小于最大值', style: TextStyle(color: Colors.red)),
  288. );
  289. }
  290. List<LineChartBarData> lineBars = [];
  291. for (int i = 0; i < _functionControllers.length; i++) {
  292. String function = _functionControllers[i].text.trim();
  293. if (function.isEmpty) continue;
  294. List<FlSpot> spots = _evaluateFunction(function, xMin, xMax, i);
  295. if (spots.isNotEmpty) {
  296. lineBars.add(
  297. LineChartBarData(
  298. spots: spots,
  299. isCurved: true,
  300. color: _functionColors[i],
  301. barWidth: 2,
  302. isStrokeCapRound: true,
  303. dotData: FlDotData(show: false),
  304. belowBarData: BarAreaData(show: false),
  305. ),
  306. );
  307. }
  308. }
  309. if (lineBars.isEmpty) {
  310. return Center(
  311. child: Column(
  312. mainAxisAlignment: MainAxisAlignment.center,
  313. children: [
  314. Icon(Icons.functions, size: 50, color: Colors.grey),
  315. SizedBox(height: 10),
  316. Text('暂无函数图像可显示', style: TextStyle(color: Colors.grey)),
  317. Text('请输入有效的函数表达式', style: TextStyle(color: Colors.grey, fontSize: 12)),
  318. ],
  319. ),
  320. );
  321. }
  322. return LineChart(
  323. LineChartData(
  324. clipData: FlClipData.all(),
  325. gridData: FlGridData(
  326. show: true,
  327. drawVerticalLine: true,
  328. drawHorizontalLine: true,
  329. getDrawingHorizontalLine: (value) {
  330. return FlLine(
  331. color: Colors.grey.withOpacity(0.3),
  332. strokeWidth: 1,
  333. );
  334. },
  335. getDrawingVerticalLine: (value) {
  336. return FlLine(
  337. color: Colors.grey.withOpacity(0.3),
  338. strokeWidth: 1,
  339. );
  340. },
  341. ),
  342. titlesData: FlTitlesData(
  343. show: true,
  344. bottomTitles: AxisTitles(
  345. sideTitles: SideTitles(
  346. showTitles: true,
  347. reservedSize: 30,
  348. interval: _calculateInterval(xMin, xMax),
  349. getTitlesWidget: (value, meta) {
  350. return Text(
  351. value.toStringAsFixed(1),
  352. style: TextStyle(fontSize: 10, color: Colors.black),
  353. );
  354. },
  355. ),
  356. ),
  357. leftTitles: AxisTitles(
  358. sideTitles: SideTitles(
  359. showTitles: true,
  360. reservedSize: 40,
  361. interval: _calculateInterval(yMin, yMax),
  362. getTitlesWidget: (value, meta) {
  363. return Text(
  364. value.toStringAsFixed(1),
  365. style: TextStyle(fontSize: 10, color: Colors.black),
  366. );
  367. },
  368. ),
  369. ),
  370. rightTitles: AxisTitles(sideTitles: SideTitles(showTitles: false)),
  371. topTitles: AxisTitles(sideTitles: SideTitles(showTitles: false)),
  372. ),
  373. borderData: FlBorderData(
  374. show: true,
  375. border: Border.all(color: Colors.black, width: 2),
  376. ),
  377. minX: xMin,
  378. maxX: xMax,
  379. minY: yMin,
  380. maxY: yMax,
  381. lineBarsData: lineBars,
  382. lineTouchData: LineTouchData(enabled: false),
  383. extraLinesData: ExtraLinesData(
  384. horizontalLines: [
  385. HorizontalLine(
  386. y: 0,
  387. color: Colors.black,
  388. strokeWidth: 1,
  389. ),
  390. ],
  391. verticalLines: [
  392. VerticalLine(
  393. x: 0,
  394. color: Colors.black,
  395. strokeWidth: 1,
  396. ),
  397. ],
  398. ),
  399. ),
  400. );
  401. }
  402. double _calculateInterval(double min, double max) {
  403. double range = max - min;
  404. if (range <= 0) return 1.0;
  405. if (range <= 2) return 0.5;
  406. if (range <= 5) return 1.0;
  407. if (range <= 10) return 2.0;
  408. if (range <= 20) return 5.0;
  409. if (range <= 50) return 10.0;
  410. if (range <= 100) return 20.0;
  411. return 50.0;
  412. }
  413. }