فهرست منبع

键盘:C 直接显示(shift 是 P)、阶乘收进括号的 shift、π 与 i 换位;
圆的方程改求半径;函数绘图能画多条曲线、范围滑条搬到「计算」上方

- 组合数 C 挪到基座(原来在 `!` 的 shift 里),它的 shift 是排列数 P;
阶乘收进 `(` 的 shift(用得比组合数少)。π 和 i 互换位置
- 圆的方程:半径平方 → **半径** √(D²+E²−4F)/2,精确值仍是根号/分数,
旁边配近似值(真机上 (0,0)(2,0)(0,2) 给的是 Sqrt(2) ≈ 1.4142)
- 函数绘图修了两个问题:
· **画不出多条曲线**:图表原来是个有状态的 widget,只在第一帧吃了 series,
后来加的函数进不去(改成无状态的 CurvePlot,series 每次重画都是新的)
· 横轴/纵轴范围两个滑动条搬到「计算」上方 —— **先定范围再画图**,
采样区间和显示窗口都用它(原来滑条在图下面、只管显示窗口)
· 顺带补回第一个输入框的默认 x^2(丢了之后打开就报"请至少填一个表达式")

Co-Authored-By: Claude Code <noreply@anthropic.com>

nonvioce 3 هفته پیش
والد
کامیت
d3c651c919
5فایلهای تغییر یافته به همراه200 افزوده شده و 167 حذف شده
  1. 9 8
      lib/calculator_page.dart
  2. 91 37
      lib/curve_plot.dart
  3. 3 3
      lib/tools/circle_equation.dart
  4. 67 116
      lib/tools/function_plotter.dart
  5. 30 3
      test/widget_test.dart

+ 9 - 8
lib/calculator_page.dart

@@ -486,9 +486,11 @@ class _CalculatorPageState extends State<CalculatorPage> {
                 shifted: _Key('atan', _Act.fn, payload: 'ArcTan')),
             _Key('ln', _Act.fn, payload: 'Log',
                 shifted: _Key('lg', _Act.fn, payload: 'Log10')),
-            _Key('!', _Act.postfix,
-                shifted: _Key('C', _Act.fnPrefixed,
-                    payload: 'Binomial', prefill: ['', ''])),
+            // 组合数直接显示,shift 才是排列数
+            _Key('C', _Act.fnPrefixed, payload: 'Binomial',
+                prefill: ['', ''],
+                shifted: _Key('P', _Act.fnPrefixed,
+                    payload: 'FactorialPower', prefill: ['', ''])),
           ],
           // 3 变量。原来那排运算符(赋值、等号、代入、逗号、矩阵)收进它们的 shift,
           //   于是整排省掉一个 —— 键盘从 9 排压到 8 排,键也更高。
@@ -509,10 +511,10 @@ class _CalculatorPageState extends State<CalculatorPage> {
             _Key('7', _Act.char, payload: '7', mergeDigit: true),
             _Key('8', _Act.char, payload: '8', mergeDigit: true),
             _Key('9', _Act.char, payload: '9', mergeDigit: true),
+            // 阶乘收进括号的 shift(用得比组合数少)
             _Key('(', _Act.group,
-                shifted: _Key('P', _Act.fnPrefixed,
-                    payload: 'FactorialPower', prefill: ['', ''])),
-            _Key('π', _Act.char, payload: 'Pi', tint: true, display: 'π'),
+                shifted: _Key('!', _Act.postfix)),
+            _Key('i', _Act.char, payload: 'I', tint: true, display: 'i'),
           ],
           [
             _Key('4', _Act.char, payload: '4', mergeDigit: true),
@@ -534,8 +536,7 @@ class _CalculatorPageState extends State<CalculatorPage> {
             _Key('^', _Act.power),
             // 常量。编辑框显示 π / e,送进引擎的是 Pi / E(和 ANS 同一套机制)
             _Key('e', _Act.char, payload: 'E', tint: true, display: 'e'),
-            // 虚数单位。送进 Symja 的是 I,编辑框里显示小写 i
-            _Key('i', _Act.char, payload: 'I', tint: true, display: 'i'),
+            _Key('π', _Act.char, payload: 'Pi', tint: true, display: 'π'),
           ],
           [
             // ⇧ 开着的时候这个键是深色的 —— 一眼能看出"现在是 shift 层"

+ 91 - 37
lib/curve_plot.dart

@@ -32,17 +32,18 @@ class CurvePlot extends StatefulWidget {
     this.yMax,
     this.showControls = true,
     this.equalAspect = false,
-    this.onViewChanged,
+    this.xWindow,
+    this.yWindow,
   });
 
   final List<CurveSeries> series;
 
-  /// 横向窗口变了就报一声,让调用方有机会**重新采样**。
+  /// 由调用方**定死**的显示窗口(给了就不自动定界、也不用下面的滑条)。
   ///
-  /// 采样区间是有限的(函数图像只有 [-10, 10]),滑动条一拉出去曲线就断了 ——
-  /// 用户实测反馈"调整滑动条后图像不会自动延展"。谁采的样谁负责延展,
-  /// 所以这里只报信,不自己采。
-  final void Function(double x0, double x1)? onViewChanged;
+  /// 函数绘图工具把两个范围滑动条搬到了「计算」上方:采样区间和显示窗口
+  /// 都由那里决定,图自己就不该再有一套滑条了(两套会互相打架)。
+  final (double, double)? xWindow;
+  final (double, double)? yWindow;
 
   /// 纵轴的**初始**窗口(不给就按数据自动定界)。之后由滑动条接管。
   final double? yMin;
@@ -74,7 +75,11 @@ class _Bounds {
 /// 抽成纯函数是为了能直接测 —— "圆是不是圆的"靠截图看像素太难回归了。
 /// 同跨度意味着同样的单位长度在两轴上对应同样多的像素。
 (double, double, double, double) equalizeAspect(
-    double x0, double x1, double y0, double y1) {
+  double x0,
+  double x1,
+  double y0,
+  double y1,
+) {
   // 退化成一点(或非法值)时给个最小跨度,否则返回的还是零跨度
   var span = math.max(x1 - x0, y1 - y0);
   if (!span.isFinite || span <= 0) span = 1;
@@ -148,10 +153,10 @@ class _CurvePlotState extends State<CurvePlot> {
   }
 
   void _reset() => setState(() {
-        _x0 = _x1 = null;
-        _y0 = widget.yMin ?? _dataY?.min;
-        _y1 = widget.yMax ?? _dataY?.max;
-      });
+    _x0 = _x1 = null;
+    _y0 = widget.yMin ?? _dataY?.min;
+    _y1 = widget.yMax ?? _dataY?.max;
+  });
 
   @override
   Widget build(BuildContext context) {
@@ -161,8 +166,10 @@ class _CurvePlotState extends State<CurvePlot> {
       return const Center(child: Text('没有可绘制的有限点'));
     }
 
-    final x0 = _x0 ?? dx.min, x1 = _x1 ?? dx.max;
-    final y0 = _y0 ?? dy.min, y1 = _y1 ?? dy.max;
+    final x0 = widget.xWindow?.$1 ?? _x0 ?? dx.min;
+    final x1 = widget.xWindow?.$2 ?? _x1 ?? dx.max;
+    final y0 = widget.yWindow?.$1 ?? _y0 ?? dy.min;
+    final y1 = widget.yWindow?.$2 ?? _y1 ?? dy.max;
 
     return Column(
       children: [
@@ -170,39 +177,61 @@ class _CurvePlotState extends State<CurvePlot> {
           child: ClipRect(
             child: CustomPaint(
               painter: _CurvePainter(
-                  widget.series, x0, x1, y0, y1, widget.equalAspect),
+                widget.series,
+                x0,
+                x1,
+                y0,
+                y1,
+                widget.equalAspect,
+              ),
               child: const SizedBox.expand(),
             ),
           ),
         ),
-        if (widget.showControls) _controls(dx, dy, x0, x1, y0, y1),
+        if (widget.showControls &&
+            widget.xWindow == null &&
+            widget.yWindow == null)
+          _controls(dx, dy, x0, x1, y0, y1),
       ],
     );
   }
 
-  Widget _controls(_Bounds dx, _Bounds dy,
-      double x0, double x1, double y0, double y1) {
+  Widget _controls(
+    _Bounds dx,
+    _Bounds dy,
+    double x0,
+    double x1,
+    double y0,
+    double y1,
+  ) {
     // 滑条可调范围比数据范围宽一倍,这样既能放大也能缩小
     final xLo = dx.min - dx.span, xHi = dx.max + dx.span;
     final yLo = dy.min - dy.span, yHi = dy.max + dy.span;
 
     return Column(
       children: [
-        _slider('横向', xLo, xHi, x0, x1, (v) {
-          setState(() {
+        _slider(
+          '横向',
+          xLo,
+          xHi,
+          x0,
+          x1,
+          (v) => setState(() {
             _x0 = v.start;
             _x1 = v.end;
-          });
-          widget.onViewChanged?.call(v.start, v.end);
-        }),
-        _slider('纵向', yLo, yHi, y0, y1, (v) {
-          setState(() {
+          }),
+        ),
+        _slider(
+          '纵向',
+          yLo,
+          yHi,
+          y0,
+          y1,
+          (v) => setState(() {
             _y0 = v.start;
             _y1 = v.end;
-          });
-          // 纵向变化不影响 x 方向要不要补采样,但一起报出去省得调用方还要判断
-          widget.onViewChanged?.call(x0, x1);
-        }),
+          }),
+        ),
         Align(
           alignment: Alignment.centerRight,
           child: TextButton.icon(
@@ -215,8 +244,14 @@ class _CurvePlotState extends State<CurvePlot> {
     );
   }
 
-  Widget _slider(String label, double lo, double hi, double start, double end,
-      void Function(RangeValues) onChanged) {
+  Widget _slider(
+    String label,
+    double lo,
+    double hi,
+    double start,
+    double end,
+    void Function(RangeValues) onChanged,
+  ) {
     return Row(
       children: [
         SizedBox(
@@ -249,7 +284,13 @@ class _CurvePlotState extends State<CurvePlot> {
 
 class _CurvePainter extends CustomPainter {
   _CurvePainter(
-      this.series, this.x0, this.x1, this.y0, this.y1, this.equalAspect);
+    this.series,
+    this.x0,
+    this.x1,
+    this.y0,
+    this.y1,
+    this.equalAspect,
+  );
 
   final List<CurveSeries> series;
   final double x0, x1, y0, y1;
@@ -336,8 +377,12 @@ class _CurvePainter extends CustomPainter {
     if (series.length > 1) _legend(canvas, plot);
   }
 
-  void _grid(Canvas canvas, Rect plot, double Function(double) sx,
-      double Function(double) sy) {
+  void _grid(
+    Canvas canvas,
+    Rect plot,
+    double Function(double) sx,
+    double Function(double) sy,
+  ) {
     final paint = Paint()
       ..color = const Color(0xFFE8E8EE)
       ..strokeWidth = 1;
@@ -358,8 +403,12 @@ class _CurvePainter extends CustomPainter {
     }
   }
 
-  void _axisLabels(Canvas canvas, Rect plot, double Function(double) sx,
-      double Function(double) sy) {
+  void _axisLabels(
+    Canvas canvas,
+    Rect plot,
+    double Function(double) sx,
+    double Function(double) sy,
+  ) {
     const c = Color(0xFF757575);
     final stepX = _niceStep(x1 - x0);
     for (var x = (x0 / stepX).ceil() * stepX; x <= x1; x += stepX) {
@@ -382,8 +431,13 @@ class _CurvePainter extends CustomPainter {
         Rect.fromLTWH(plot.right - 108, y + 3, 14, 3),
         Paint()..color = s.color,
       );
-      _text(canvas, s.label, Offset(plot.right - 88, y - 3),
-          const Color(0xFF616161), 10);
+      _text(
+        canvas,
+        s.label,
+        Offset(plot.right - 88, y - 3),
+        const Color(0xFF616161),
+        10,
+      );
       y += 14;
     }
   }

+ 3 - 3
lib/tools/circle_equation.dart

@@ -51,11 +51,11 @@ class _CircleEquationPageState extends State<CircleEquationPage> {
         return [ToolText('结果', '${defs.isEmpty ? '' : defs.first}')];
       }
 
-      // 圆心 (-dd/2, -ee/2) 与半径平方 (dd²+ee²-4ff)/4
+      // 圆心 (-dd/2, -ee/2) 与半径 √(dd²+ee²-4ff)/2
       final centerExpr =
           'With[{$bind}, Module[{s}, s=Solve[$eqs,{dd,ee,ff}]; '
           'If[s==={}, $noCircle, '
-          '{-dd/2, -ee/2, (dd^2+ee^2-4*ff)/4} /. First[s]]]]';
+          '{-dd/2, -ee/2, Sqrt[dd^2+ee^2-4*ff]/2} /. First[s]]]]';
 
       // 精确值 + 近似值都要:系数和圆心半径都是分数,小数形式有时更直观
       final defsN = parseSymjaList(
@@ -86,7 +86,7 @@ class _CircleEquationPageState extends State<CircleEquationPage> {
                 '${withApprox('${center[1]}', at(centerN, 1))})',
           ),
         );
-        out.add(ToolText('半径平方', withApprox('${center[2]}', at(centerN, 2))));
+        out.add(ToolText('半径', withApprox('${center[2]}', at(centerN, 2))));
       }
       return out;
     },

+ 67 - 116
lib/tools/function_plotter.dart

@@ -4,8 +4,6 @@
 //
 // 采样部分(区间、点数、由 sample 拿点)照旧不变;变的只是外壳 —— 输入框交给
 // [ToolScaffold],绘制按钮就是外壳的「计算」,重置按钮也是外壳自带的。
-import 'dart:math' as math;
-
 import 'package:flutter/material.dart';
 
 import '../curve_plot.dart';
@@ -20,11 +18,17 @@ class FunctionPlotterPage extends StatefulWidget {
 }
 
 class _FunctionPlotterPageState extends State<FunctionPlotterPage> {
-  /// 固定的采样区间与点数。横纵范围由图下面的滑条控制,点取得密一些,
-  /// 放大到一小段时也还有足够的点。
-  static const double _sampleFrom = -10;
-  static const double _sampleTo = 10;
-  static const int _sampleCount = 800;
+  /// 横纵范围在**画图之前**定:两个滑动条放在「计算」按钮上方,
+  /// 采样区间和显示窗口都用它们(原来滑条在图下面、只管显示窗口,
+  /// 曲线拉出去就断在那儿)。
+  RangeValues _xRange = const RangeValues(-10, 10);
+  RangeValues _yRange = const RangeValues(-10, 10);
+
+  /// 滑条的取值范围(比默认宽,方便拉大拉小)。
+  static const RangeValues _rangeLimits = RangeValues(-50, 50);
+
+  /// 每单位 x 采几个点。
+  static const double _perUnit = 60;
 
   static const int _maxCurves = 5;
 
@@ -47,6 +51,13 @@ class _FunctionPlotterPageState extends State<FunctionPlotterPage> {
     ),
   ];
 
+  @override
+  void initState() {
+    super.initState();
+    // 打开就该看到一条曲线:第一个框预置 x^2(autoRun 立刻画)
+    _exprs.first.controller.text = 'x^2';
+  }
+
   void _addCurve() {
     if (_exprs.length >= _maxCurves) return;
     setState(() {
@@ -67,9 +78,45 @@ class _FunctionPlotterPageState extends State<FunctionPlotterPage> {
     setState(() => _exprs.removeLast());
   }
 
+  /// 一条范围滑动条:左边名字、中间滑条、右边当前范围。
+  Widget _rangeRow(
+    String label,
+    RangeValues v,
+    ValueChanged<RangeValues> onChanged,
+  ) => Row(
+    children: [
+      SizedBox(
+        width: 64,
+        child: Text(label, style: const TextStyle(fontSize: 12)),
+      ),
+      Expanded(
+        child: RangeSlider(
+          values: v,
+          min: _rangeLimits.start,
+          max: _rangeLimits.end,
+          divisions: (_rangeLimits.end - _rangeLimits.start).round(),
+          labels: RangeLabels('${v.start.round()}', '${v.end.round()}'),
+          onChanged: onChanged,
+        ),
+      ),
+      SizedBox(
+        width: 76,
+        child: Text(
+          '${v.start.round()} ~ ${v.end.round()}',
+          textAlign: TextAlign.right,
+          style: const TextStyle(fontSize: 12, fontFamily: 'monospace'),
+        ),
+      ),
+    ],
+  );
+
+  /// 两个范围滑动条放在「计算」上方:**先定范围再画图**。
   Widget _intro() => Column(
     crossAxisAlignment: CrossAxisAlignment.start,
     children: [
+      _rangeRow('横轴范围', _xRange, (v) => setState(() => _xRange = v)),
+      _rangeRow('纵轴范围', _yRange, (v) => setState(() => _yRange = v)),
+      const SizedBox(height: 8),
       Row(
         children: [
           OutlinedButton.icon(
@@ -96,28 +143,28 @@ class _FunctionPlotterPageState extends State<FunctionPlotterPage> {
     fields: _exprs,
     intro: _intro(),
     compute: (session) async {
-      final curves = <({String expr, Color color})>[];
       final series = <CurveSeries>[];
       final failed = <String>[];
+      final from = _xRange.start, to = _xRange.end;
+      final count = ((to - from) * _perUnit).round().clamp(200, 4000);
+
       // 逐条采样:一条失败只记下它自己,别的照画。
       for (var i = 0; i < _exprs.length; i++) {
         final raw = _exprs[i].text;
         if (raw.isEmpty) continue;
-        final color = _curveColors[i % _curveColors.length];
         try {
           // 表达式原样交给 Symja 解析,谁解析谁负责。
           final s = await session.sample(
             raw,
             variable: 'x',
-            from: _sampleFrom,
-            to: _sampleTo,
-            count: _sampleCount,
+            from: from,
+            to: to,
+            count: count,
           );
-          curves.add((expr: raw, color: color));
           series.add(
             CurveSeries(
               label: raw,
-              color: color,
+              color: _curveColors[i % _curveColors.length],
               // 非有限值(极点处的 ComplexInfinity 等)原样保留,
               // CurvePlot 会把它们当成抬笔断点。
               points: <Offset>[
@@ -140,12 +187,12 @@ class _FunctionPlotterPageState extends State<FunctionPlotterPage> {
             // 否则放在外层 Column 里会因为高度无界而报错。
             SizedBox(
               height: 360,
-              child: _FunctionChart(
-                session: session,
-                curves: curves,
-                initialSeries: series,
-                initialFrom: _sampleFrom,
-                initialTo: _sampleTo,
+              child: CurvePlot(
+                series: series,
+                // 窗口由上面两个滑动条定死,图自己的滑条收起来
+                xWindow: (from, to),
+                yWindow: (_yRange.start, _yRange.end),
+                showControls: false,
               ),
             ),
           ),
@@ -154,99 +201,3 @@ class _FunctionPlotterPageState extends State<FunctionPlotterPage> {
     },
   );
 }
-
-/// 会跟着横向滑动条**重新采样**的函数图像。
-///
-/// 采样区间原来是写死的 [-10, 10],滑动条一拉出去曲线就断在那儿了(用户实测
-/// 反馈"调整滑动条后图像不会自动延展")。这里接住 CurvePlot 报回来的窗口,
-/// 只要视野跑出已采样的范围就往外扩一圈、把函数重新采一遍 —— 视野里永远有曲线。
-class _FunctionChart extends StatefulWidget {
-  const _FunctionChart({
-    required this.session,
-    required this.curves,
-    required this.initialSeries,
-    required this.initialFrom,
-    required this.initialTo,
-  });
-
-  final SymjaSession session;
-
-  /// 要画的函数(表达式 + 颜色)。重新采样时按同一份清单再采一遍。
-  final List<({String expr, Color color})> curves;
-
-  /// 页面已经采好的第一批点,免得这里再采一次。
-  final List<CurveSeries> initialSeries;
-
-  final double initialFrom;
-  final double initialTo;
-
-  @override
-  State<_FunctionChart> createState() => _FunctionChartState();
-}
-
-class _FunctionChartState extends State<_FunctionChart> {
-  /// 每单位 x 采几个点。放大到一小段时也还画得平滑。
-  static const double _perUnit = 60;
-
-  late double _from = widget.initialFrom;
-  late double _to = widget.initialTo;
-  late List<CurveSeries> _series = widget.initialSeries;
-
-  bool _sampling = false;
-
-  /// 视野跑出已采样范围(两边各留 10% 余量)才动手,否则拖动过程中会一直重采。
-  void _onView(double x0, double x1) {
-    final span = _to - _from;
-    if (x0 >= _from + span * 0.1 && x1 <= _to - span * 0.1) return;
-    final from = math.min(_from, x0 - span * 0.15);
-    final to = math.max(_to, x1 + span * 0.15);
-    _resample(from, to);
-  }
-
-  Future<void> _resample(double from, double to) async {
-    if (_sampling) return; // 采样期间不排队:松开滑条后还会再报一次
-    _sampling = true;
-    try {
-      final count = ((to - from) * _perUnit).round().clamp(400, 4000);
-      final out = <CurveSeries>[];
-      for (final c in widget.curves) {
-        try {
-          final s = await widget.session.sample(
-            c.expr,
-            variable: 'x',
-            from: from,
-            to: to,
-            count: count,
-          );
-          out.add(
-            CurveSeries(
-              label: c.expr,
-              color: c.color,
-              points: <Offset>[
-                for (var k = 0; k < s.ys.length; k++) Offset(s.xAt(k), s.ys[k]),
-              ],
-            ),
-          );
-        } on SymjaException {
-          // 拉远之后算不动了(比如跑到定义域外、或被取消):留着原来的曲线,
-          // 别把已经画出来的图清空。
-        }
-      }
-      if (!mounted) return;
-      // 全都失败就当这次没发生过 —— 至少不会把图变空
-      if (out.length == widget.curves.length) {
-        setState(() {
-          _from = from;
-          _to = to;
-          _series = out;
-        });
-      }
-    } finally {
-      _sampling = false;
-    }
-  }
-
-  @override
-  Widget build(BuildContext context) =>
-      CurvePlot(series: _series, onViewChanged: _onView);
-}

+ 30 - 3
test/widget_test.dart

@@ -6,11 +6,13 @@ import 'package:flutter_test/flutter_test.dart';
 
 import 'package:cal/calculator_page.dart';
 import 'package:cal/console_page.dart';
+import 'package:cal/curve_plot.dart';
 import 'package:cal/expr_editor.dart';
 import 'package:cal/main.dart';
 import 'package:cal/settings.dart';
 import 'package:cal/tools/cuo_wei_xiang_jian.dart';
 import 'package:cal/tools/cycle_coloring.dart';
+import 'package:cal/tools/function_plotter.dart';
 import 'package:cal/tools/group_allocation.dart';
 import 'package:cal/tools_page.dart';
 
@@ -361,8 +363,7 @@ void main() {
     testWidgets('shift 层:C 是组合数、P 是排列数', (tester) async {
       await pumpApp(tester);
 
-      await press(tester, '⇧');
-      await press(tester, 'C'); // Shif 层的 Binomial[□, □]
+      await press(tester, 'C'); // 组合数直接显示:Binomial[□, □]
       await press(tester, '5');
       await pressShift(tester, ',');
       await press(tester, '2');
@@ -370,13 +371,20 @@ void main() {
 
       await press(tester, 'CLR');
       await press(tester, '⇧');
-      await press(tester, 'P'); // FactorialPower[□, □]
+      await press(tester, 'P'); // 排列数是 C 的 shift:FactorialPower[□, □]
       await press(tester, '5');
       await pressShift(tester, ',');
       await press(tester, '2');
       expect(await lastExpr(tester), 'FactorialPower[5, 2]');
     });
 
+    testWidgets('阶乘收在括号的 shift 里', (tester) async {
+      await pumpApp(tester);
+      await press(tester, '5');
+      await pressShift(tester, '!'); // `(` 的 shift
+      expect(await lastExpr(tester), '5!');
+    });
+
     testWidgets('shift 只吃一次:按完一个键就复位', (tester) async {
       await pumpApp(tester);
 
@@ -669,6 +677,25 @@ void main() {
       );
     });
 
+    testWidgets('函数图像能同时画多条曲线', (tester) async {
+      await openTool(tester, '函数图像绘制');
+      await tester.tap(find.widgetWithText(OutlinedButton, '添加函数'));
+      await tester.pumpAndSettle();
+
+      final fields = find.descendant(
+        of: find.byType(FunctionPlotterPage),
+        matching: find.byType(TextField),
+      );
+      await tester.enterText(fields.at(1), 'Sin[x]');
+      await runTool(tester);
+
+      // 回归:图表原来是个有状态的 widget,只在第一帧拿了 series,
+      // 后来加的函数根本进不去
+      final plot = tester.widget<CurvePlot>(find.byType(CurvePlot));
+      expect(plot.series.map((s) => s.label), containsAll(['x^2', 'Sin[x]']));
+      expect(plot.series.length, 2);
+    });
+
     testWidgets('输出点一下就复制(图表除外)', (tester) async {
       final copied = <String>[];
       TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger