✨ Function chart
This commit is contained in:
		| @@ -2,6 +2,8 @@ import 'package:flutter/material.dart'; | |||||||
| import 'package:latext/latext.dart'; | import 'package:latext/latext.dart'; | ||||||
| import 'package:simple_math_calc/models/calculation_step.dart'; | import 'package:simple_math_calc/models/calculation_step.dart'; | ||||||
| import 'package:simple_math_calc/solver.dart'; | import 'package:simple_math_calc/solver.dart'; | ||||||
|  | import 'package:fl_chart/fl_chart.dart'; | ||||||
|  | import 'package:math_expressions/math_expressions.dart' as math_expressions; | ||||||
| import 'dart:math'; | import 'dart:math'; | ||||||
|  |  | ||||||
| class CalculatorHomePage extends StatefulWidget { | class CalculatorHomePage extends StatefulWidget { | ||||||
| @@ -18,19 +20,141 @@ class _CalculatorHomePageState extends State<CalculatorHomePage> { | |||||||
|  |  | ||||||
|   CalculationResult? _result; |   CalculationResult? _result; | ||||||
|   bool _isLoading = false; |   bool _isLoading = false; | ||||||
|  |   double _zoomFactor = 1.0; | ||||||
|  |  | ||||||
|   @override |   @override | ||||||
|   void initState() { |   void initState() { | ||||||
|     super.initState(); |     super.initState(); | ||||||
|     _focusNode = FocusNode(); |     _focusNode = FocusNode(); | ||||||
|  |     _controller.addListener(_onTextChanged); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   @override |   @override | ||||||
|   void dispose() { |   void dispose() { | ||||||
|  |     _controller.removeListener(_onTextChanged); | ||||||
|     _focusNode.dispose(); |     _focusNode.dispose(); | ||||||
|     super.dispose(); |     super.dispose(); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   void _onTextChanged() { | ||||||
|  |     setState(() {}); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   /// 生成函数图表的点 | ||||||
|  |   List<FlSpot> _generatePlotPoints(String expression, double zoomFactor) { | ||||||
|  |     try { | ||||||
|  |       // 如果是方程,取左边作为函数 | ||||||
|  |       String functionExpr = expression; | ||||||
|  |       if (expression.contains('=')) { | ||||||
|  |         functionExpr = expression.split('=')[0].trim(); | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       // 如果表达式不包含 x,返回空列表 | ||||||
|  |       if (!functionExpr.contains('x') && !functionExpr.contains('X')) { | ||||||
|  |         return []; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       // 预处理表达式,确保格式正确 | ||||||
|  |       functionExpr = functionExpr.replaceAll(' ', ''); | ||||||
|  |  | ||||||
|  |       // 在数字和变量之间插入乘号 | ||||||
|  |       functionExpr = functionExpr.replaceAllMapped( | ||||||
|  |         RegExp(r'(\d)([a-zA-Z])'), | ||||||
|  |         (match) => '${match.group(1)}*${match.group(2)}', | ||||||
|  |       ); | ||||||
|  |  | ||||||
|  |       // 在变量和数字之间插入乘号 (如 x2 -> x*2) | ||||||
|  |       functionExpr = functionExpr.replaceAllMapped( | ||||||
|  |         RegExp(r'([a-zA-Z])(\d)'), | ||||||
|  |         (match) => '${match.group(1)}*${match.group(2)}', | ||||||
|  |       ); | ||||||
|  |  | ||||||
|  |       // 解析表达式 | ||||||
|  |       final parser = math_expressions.ShuntingYardParser(); | ||||||
|  |       final expr = parser.parse(functionExpr); | ||||||
|  |  | ||||||
|  |       // 创建变量 x | ||||||
|  |       final x = math_expressions.Variable('x'); | ||||||
|  |  | ||||||
|  |       // 根据缩放因子动态调整范围和步长 | ||||||
|  |       final range = 10.0 * zoomFactor; | ||||||
|  |       final step = max(0.05, 0.2 / zoomFactor); // 缩放时步长更小,放大时步长更大 | ||||||
|  |  | ||||||
|  |       // 生成点 | ||||||
|  |       List<FlSpot> points = []; | ||||||
|  |       for (double i = -range; i <= range; i += step) { | ||||||
|  |         try { | ||||||
|  |           final context = math_expressions.ContextModel() | ||||||
|  |             ..bindVariable(x, math_expressions.Number(i)); | ||||||
|  |           final evaluator = math_expressions.RealEvaluator(context); | ||||||
|  |           final y = evaluator.evaluate(expr); | ||||||
|  |  | ||||||
|  |           if (y.isFinite && !y.isNaN) { | ||||||
|  |             points.add(FlSpot(i, y.toDouble())); | ||||||
|  |           } | ||||||
|  |         } catch (e) { | ||||||
|  |           // 跳过无法计算的点 | ||||||
|  |           continue; | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       // 如果没有足够的点,返回空列表 | ||||||
|  |       if (points.length < 2) { | ||||||
|  |         debugPrint('Generated ${points.length} dots'); | ||||||
|  |         return []; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       // 排序点按 x 值 | ||||||
|  |       points.sort((a, b) => a.x.compareTo(b.x)); | ||||||
|  |  | ||||||
|  |       debugPrint( | ||||||
|  |         'Generated ${points.length} dots with zoom factor $zoomFactor', | ||||||
|  |       ); | ||||||
|  |       return points; | ||||||
|  |     } catch (e) { | ||||||
|  |       debugPrint('Error generating plot points: $e'); | ||||||
|  |       return []; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   /// 计算图表的数据范围 | ||||||
|  |   ({double minX, double maxX, double minY, double maxY}) _calculateChartBounds( | ||||||
|  |     List<FlSpot> points, | ||||||
|  |     double zoomFactor, | ||||||
|  |   ) { | ||||||
|  |     if (points.isEmpty) { | ||||||
|  |       return ( | ||||||
|  |         minX: -10 * zoomFactor, | ||||||
|  |         maxX: 10 * zoomFactor, | ||||||
|  |         minY: -50 * zoomFactor, | ||||||
|  |         maxY: 50 * zoomFactor, | ||||||
|  |       ); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     double minX = points.first.x; | ||||||
|  |     double maxX = points.first.x; | ||||||
|  |     double minY = points.first.y; | ||||||
|  |     double maxY = points.first.y; | ||||||
|  |  | ||||||
|  |     for (final point in points) { | ||||||
|  |       minX = min(minX, point.x); | ||||||
|  |       maxX = max(maxX, point.x); | ||||||
|  |       minY = min(minY, point.y); | ||||||
|  |       maxY = max(maxY, point.y); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     // 添加边距 | ||||||
|  |     final xPadding = (maxX - minX) * 0.1; | ||||||
|  |     final yPadding = (maxY - minY) * 0.1; | ||||||
|  |  | ||||||
|  |     return ( | ||||||
|  |       minX: minX - xPadding, | ||||||
|  |       maxX: maxX + xPadding, | ||||||
|  |       minY: minY - yPadding, | ||||||
|  |       maxY: maxY + yPadding, | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  |  | ||||||
|   void _solveEquation() { |   void _solveEquation() { | ||||||
|     if (_controller.text.isEmpty) { |     if (_controller.text.isEmpty) { | ||||||
|       return; |       return; | ||||||
| @@ -61,6 +185,18 @@ class _CalculatorHomePageState extends State<CalculatorHomePage> { | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   void _zoomIn() { | ||||||
|  |     setState(() { | ||||||
|  |       _zoomFactor = (_zoomFactor * 0.8).clamp(0.1, 10.0); | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   void _zoomOut() { | ||||||
|  |     setState(() { | ||||||
|  |       _zoomFactor = (_zoomFactor * 1.25).clamp(0.1, 10.0); | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  |  | ||||||
|   @override |   @override | ||||||
|   Widget build(BuildContext context) { |   Widget build(BuildContext context) { | ||||||
|     return Scaffold( |     return Scaffold( | ||||||
| @@ -218,6 +354,101 @@ class _CalculatorHomePageState extends State<CalculatorHomePage> { | |||||||
|             ), |             ), | ||||||
|           ), |           ), | ||||||
|         ), |         ), | ||||||
|  |         const SizedBox(height: 16), | ||||||
|  |         Card( | ||||||
|  |           child: Padding( | ||||||
|  |             padding: const EdgeInsets.all(16.0), | ||||||
|  |             child: Column( | ||||||
|  |               children: [ | ||||||
|  |                 Row( | ||||||
|  |                   mainAxisAlignment: MainAxisAlignment.spaceBetween, | ||||||
|  |                   children: [ | ||||||
|  |                     Padding( | ||||||
|  |                       padding: const EdgeInsets.only(left: 4), | ||||||
|  |                       child: Text( | ||||||
|  |                         '函数图像', | ||||||
|  |                         style: Theme.of(context).textTheme.titleMedium, | ||||||
|  |                       ), | ||||||
|  |                     ), | ||||||
|  |                     Row( | ||||||
|  |                       mainAxisAlignment: MainAxisAlignment.center, | ||||||
|  |                       children: [ | ||||||
|  |                         IconButton( | ||||||
|  |                           onPressed: _zoomIn, | ||||||
|  |                           icon: Icon(Icons.zoom_in), | ||||||
|  |                           tooltip: '放大', | ||||||
|  |                           padding: EdgeInsets.zero, | ||||||
|  |                           visualDensity: VisualDensity.compact, | ||||||
|  |                         ), | ||||||
|  |                         IconButton( | ||||||
|  |                           onPressed: _zoomOut, | ||||||
|  |                           icon: Icon(Icons.zoom_out), | ||||||
|  |                           tooltip: '缩小', | ||||||
|  |                           padding: EdgeInsets.zero, | ||||||
|  |                           visualDensity: VisualDensity.compact, | ||||||
|  |                         ), | ||||||
|  |                       ], | ||||||
|  |                     ), | ||||||
|  |                   ], | ||||||
|  |                 ), | ||||||
|  |                 const SizedBox(height: 24), | ||||||
|  |                 SizedBox( | ||||||
|  |                   height: 300, | ||||||
|  |                   child: Builder( | ||||||
|  |                     builder: (context) { | ||||||
|  |                       final points = _generatePlotPoints( | ||||||
|  |                         _controller.text, | ||||||
|  |                         _zoomFactor, | ||||||
|  |                       ); | ||||||
|  |                       final bounds = _calculateChartBounds(points, _zoomFactor); | ||||||
|  |  | ||||||
|  |                       return LineChart( | ||||||
|  |                         LineChartData( | ||||||
|  |                           gridData: FlGridData(show: true), | ||||||
|  |                           titlesData: FlTitlesData( | ||||||
|  |                             leftTitles: AxisTitles( | ||||||
|  |                               sideTitles: SideTitles( | ||||||
|  |                                 showTitles: true, | ||||||
|  |                                 reservedSize: 40, | ||||||
|  |                               ), | ||||||
|  |                             ), | ||||||
|  |                             bottomTitles: AxisTitles( | ||||||
|  |                               sideTitles: SideTitles( | ||||||
|  |                                 showTitles: true, | ||||||
|  |                                 reservedSize: 30, | ||||||
|  |                               ), | ||||||
|  |                             ), | ||||||
|  |                             topTitles: AxisTitles( | ||||||
|  |                               sideTitles: SideTitles(showTitles: false), | ||||||
|  |                             ), | ||||||
|  |                             rightTitles: AxisTitles( | ||||||
|  |                               sideTitles: SideTitles(showTitles: false), | ||||||
|  |                             ), | ||||||
|  |                           ), | ||||||
|  |                           borderData: FlBorderData(show: true), | ||||||
|  |                           lineBarsData: [ | ||||||
|  |                             LineChartBarData( | ||||||
|  |                               spots: points, | ||||||
|  |                               isCurved: true, | ||||||
|  |                               color: Theme.of(context).colorScheme.primary, | ||||||
|  |                               barWidth: 3, | ||||||
|  |                               belowBarData: BarAreaData(show: false), | ||||||
|  |                               dotData: FlDotData(show: false), | ||||||
|  |                             ), | ||||||
|  |                           ], | ||||||
|  |                           minX: bounds.minX, | ||||||
|  |                           maxX: bounds.maxX, | ||||||
|  |                           minY: bounds.minY, | ||||||
|  |                           maxY: bounds.maxY, | ||||||
|  |                         ), | ||||||
|  |                       ); | ||||||
|  |                     }, | ||||||
|  |                   ), | ||||||
|  |                 ), | ||||||
|  |               ], | ||||||
|  |             ), | ||||||
|  |           ), | ||||||
|  |         ), | ||||||
|       ], |       ], | ||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
|   | |||||||
							
								
								
									
										16
									
								
								pubspec.lock
									
									
									
									
									
								
							
							
						
						
									
										16
									
								
								pubspec.lock
									
									
									
									
									
								
							| @@ -145,6 +145,14 @@ packages: | |||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "1.0.8" |     version: "1.0.8" | ||||||
|  |   equatable: | ||||||
|  |     dependency: transitive | ||||||
|  |     description: | ||||||
|  |       name: equatable | ||||||
|  |       sha256: "567c64b3cb4cf82397aac55f4f0cbd3ca20d77c6c03bedbc4ceaddc08904aef7" | ||||||
|  |       url: "https://pub.dev" | ||||||
|  |     source: hosted | ||||||
|  |     version: "2.0.7" | ||||||
|   fake_async: |   fake_async: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
| @@ -169,6 +177,14 @@ packages: | |||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "7.0.1" |     version: "7.0.1" | ||||||
|  |   fl_chart: | ||||||
|  |     dependency: "direct main" | ||||||
|  |     description: | ||||||
|  |       name: fl_chart | ||||||
|  |       sha256: "00b74ae680df6b1135bdbea00a7d1fc072a9180b7c3f3702e4b19a9943f5ed7d" | ||||||
|  |       url: "https://pub.dev" | ||||||
|  |     source: hosted | ||||||
|  |     version: "0.66.2" | ||||||
|   flutter: |   flutter: | ||||||
|     dependency: "direct main" |     dependency: "direct main" | ||||||
|     description: flutter |     description: flutter | ||||||
|   | |||||||
| @@ -40,6 +40,7 @@ dependencies: | |||||||
|   go_router: ^16.2.1 |   go_router: ^16.2.1 | ||||||
|   url_launcher: ^6.3.2 |   url_launcher: ^6.3.2 | ||||||
|   rational: ^2.2.3 |   rational: ^2.2.3 | ||||||
|  |   fl_chart: ^0.66.1 | ||||||
|  |  | ||||||
| dev_dependencies: | dev_dependencies: | ||||||
|   flutter_test: |   flutter_test: | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user