|
|
@@ -31,6 +31,7 @@ class CurvePlot extends StatefulWidget {
|
|
|
this.yMin,
|
|
|
this.yMax,
|
|
|
this.showControls = true,
|
|
|
+ this.equalAspect = false,
|
|
|
});
|
|
|
|
|
|
final List<CurveSeries> series;
|
|
|
@@ -42,6 +43,12 @@ class CurvePlot extends StatefulWidget {
|
|
|
/// 是否显示下面的滑动条。嵌在很小的对话框里时可以关掉。
|
|
|
final bool showControls;
|
|
|
|
|
|
+ /// 等比例:两轴用**同样的单位长度**(圆看起来才是圆)。
|
|
|
+ ///
|
|
|
+ /// 参数方程、圆、极坐标这类图必须开 —— 默认关掉是因为函数图像
|
|
|
+ /// 更希望横向铺满,而不是为了保持比例留出大片空白。
|
|
|
+ final bool equalAspect;
|
|
|
+
|
|
|
@override
|
|
|
State<CurvePlot> createState() => _CurvePlotState();
|
|
|
}
|
|
|
@@ -54,6 +61,25 @@ class _Bounds {
|
|
|
double get span => max - min;
|
|
|
}
|
|
|
|
|
|
+/// 把两轴范围调成**同跨度、同中心**(各边留 5% 余量)。
|
|
|
+///
|
|
|
+/// 抽成纯函数是为了能直接测 —— "圆是不是圆的"靠截图看像素太难回归了。
|
|
|
+/// 同跨度意味着同样的单位长度在两轴上对应同样多的像素。
|
|
|
+(double, double, double, double) equalizeAspect(
|
|
|
+ double x0, double x1, double y0, double y1) {
|
|
|
+ // 退化成一点(或非法值)时给个最小跨度,否则返回的还是零跨度
|
|
|
+ var span = math.max(x1 - x0, y1 - y0);
|
|
|
+ if (!span.isFinite || span <= 0) span = 1;
|
|
|
+ final pad = span * 0.05;
|
|
|
+ x0 -= pad;
|
|
|
+ x1 += pad;
|
|
|
+ y0 -= pad;
|
|
|
+ y1 += pad;
|
|
|
+ final cx = (x0 + x1) / 2, cy = (y0 + y1) / 2;
|
|
|
+ final half = math.max(x1 - x0, y1 - y0) / 2;
|
|
|
+ return (cx - half, cx + half, cy - half, cy + half);
|
|
|
+}
|
|
|
+
|
|
|
class _CurvePlotState extends State<CurvePlot> {
|
|
|
/// 当前视图窗口。null = 跟着数据自动定界。
|
|
|
double? _x0, _x1, _y0, _y1;
|
|
|
@@ -85,6 +111,12 @@ class _CurvePlotState extends State<CurvePlot> {
|
|
|
y0 -= 1;
|
|
|
y1 += 1;
|
|
|
}
|
|
|
+ if (widget.equalAspect) {
|
|
|
+ final r = equalizeAspect(x0, x1, y0, y1);
|
|
|
+ _dataX = _Bounds(r.$1, r.$2);
|
|
|
+ _dataY = _Bounds(r.$3, r.$4);
|
|
|
+ return;
|
|
|
+ }
|
|
|
// 上下留 5% 余量,别让曲线顶到边
|
|
|
final pad = (y1 - y0) * 0.05;
|
|
|
_dataX = _Bounds(x0, x1);
|
|
|
@@ -129,7 +161,8 @@ class _CurvePlotState extends State<CurvePlot> {
|
|
|
Expanded(
|
|
|
child: ClipRect(
|
|
|
child: CustomPaint(
|
|
|
- painter: _CurvePainter(widget.series, x0, x1, y0, y1),
|
|
|
+ painter: _CurvePainter(
|
|
|
+ widget.series, x0, x1, y0, y1, widget.equalAspect),
|
|
|
child: const SizedBox.expand(),
|
|
|
),
|
|
|
),
|
|
|
@@ -200,10 +233,12 @@ class _CurvePlotState extends State<CurvePlot> {
|
|
|
}
|
|
|
|
|
|
class _CurvePainter extends CustomPainter {
|
|
|
- _CurvePainter(this.series, this.x0, this.x1, this.y0, this.y1);
|
|
|
+ _CurvePainter(
|
|
|
+ this.series, this.x0, this.x1, this.y0, this.y1, this.equalAspect);
|
|
|
|
|
|
final List<CurveSeries> series;
|
|
|
final double x0, x1, y0, y1;
|
|
|
+ final bool equalAspect;
|
|
|
|
|
|
static const _padLeft = 46.0;
|
|
|
static const _padRight = 12.0;
|
|
|
@@ -217,12 +252,18 @@ class _CurvePainter extends CustomPainter {
|
|
|
Paint()..color = const Color(0xFFFAFAFC),
|
|
|
);
|
|
|
|
|
|
- final plot = Rect.fromLTRB(
|
|
|
+ var plot = Rect.fromLTRB(
|
|
|
_padLeft,
|
|
|
_padTop,
|
|
|
size.width - _padRight,
|
|
|
size.height - _padBottom,
|
|
|
);
|
|
|
+ if (equalAspect) {
|
|
|
+ // 把绘图区收成正方形 —— 否则宽度比高度少掉两边的轴标签宽度,
|
|
|
+ // 画出来的圆就是椭的。
|
|
|
+ final side = math.min(plot.width, plot.height);
|
|
|
+ plot = Rect.fromCenter(center: plot.center, width: side, height: side);
|
|
|
+ }
|
|
|
|
|
|
double sx(double x) => plot.left + (x - x0) / (x1 - x0) * plot.width;
|
|
|
double sy(double y) => plot.bottom - (y - y0) / (y1 - y0) * plot.height;
|