From bd97721dbc75a396b5fb80ee313e2faebda2649a Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Sun, 14 Sep 2025 03:08:53 +0800 Subject: [PATCH] :sparkles: Function chart --- lib/screens/calculator_home_page.dart | 231 ++++++++++++++++++++++++++ pubspec.lock | 16 ++ pubspec.yaml | 1 + 3 files changed, 248 insertions(+) diff --git a/lib/screens/calculator_home_page.dart b/lib/screens/calculator_home_page.dart index c4c4450..4193f95 100644 --- a/lib/screens/calculator_home_page.dart +++ b/lib/screens/calculator_home_page.dart @@ -2,6 +2,8 @@ import 'package:flutter/material.dart'; import 'package:latext/latext.dart'; import 'package:simple_math_calc/models/calculation_step.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'; class CalculatorHomePage extends StatefulWidget { @@ -18,19 +20,141 @@ class _CalculatorHomePageState extends State { CalculationResult? _result; bool _isLoading = false; + double _zoomFactor = 1.0; @override void initState() { super.initState(); _focusNode = FocusNode(); + _controller.addListener(_onTextChanged); } @override void dispose() { + _controller.removeListener(_onTextChanged); _focusNode.dispose(); super.dispose(); } + void _onTextChanged() { + setState(() {}); + } + + /// 生成函数图表的点 + List _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 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 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() { if (_controller.text.isEmpty) { return; @@ -61,6 +185,18 @@ class _CalculatorHomePageState extends State { } } + 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 Widget build(BuildContext context) { return Scaffold( @@ -218,6 +354,101 @@ class _CalculatorHomePageState extends State { ), ), ), + 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, + ), + ); + }, + ), + ), + ], + ), + ), + ), ], ); } diff --git a/pubspec.lock b/pubspec.lock index cff86c1..5bbd9e2 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -145,6 +145,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.8" + equatable: + dependency: transitive + description: + name: equatable + sha256: "567c64b3cb4cf82397aac55f4f0cbd3ca20d77c6c03bedbc4ceaddc08904aef7" + url: "https://pub.dev" + source: hosted + version: "2.0.7" fake_async: dependency: transitive description: @@ -169,6 +177,14 @@ packages: url: "https://pub.dev" source: hosted 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: dependency: "direct main" description: flutter diff --git a/pubspec.yaml b/pubspec.yaml index 4b298ac..c647ff7 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -40,6 +40,7 @@ dependencies: go_router: ^16.2.1 url_launcher: ^6.3.2 rational: ^2.2.3 + fl_chart: ^0.66.1 dev_dependencies: flutter_test: