From fcdb81ae234b25e627a74e0307f14a5a2b0f6025 Mon Sep 17 00:00:00 2001 From: hhh <1224991097@qq.com> Date: Wed, 17 Sep 2025 17:22:37 +0800 Subject: [PATCH 01/10] feat: enhanced line chart curve function --- .../samples/bar/bar_chart_sample7.dart | 3 +- .../samples/line/line_chart_sample1.dart | 14 +- .../samples/line/line_chart_sample10.dart | 4 +- .../samples/line/line_chart_sample13.dart | 2 +- .../samples/line/line_chart_sample2.dart | 4 +- .../samples/line/line_chart_sample3.dart | 2 +- .../samples/line/line_chart_sample4.dart | 2 +- .../samples/line/line_chart_sample5.dart | 2 +- .../samples/line/line_chart_sample6.dart | 4 +- .../samples/line/line_chart_sample7.dart | 4 +- .../samples/line/line_chart_sample8.dart | 2 +- .../samples/line/line_chart_sample9.dart | 2 +- lib/fl_chart.dart | 1 + .../chart/line_chart/line_chart_curve.dart | 343 ++++++++++++++++++ lib/src/chart/line_chart/line_chart_data.dart | 114 ++++-- .../chart/line_chart/line_chart_painter.dart | 45 +-- test/chart/data_pool.dart | 74 ++-- 17 files changed, 497 insertions(+), 125 deletions(-) create mode 100644 lib/src/chart/line_chart/line_chart_curve.dart diff --git a/example/lib/presentation/samples/bar/bar_chart_sample7.dart b/example/lib/presentation/samples/bar/bar_chart_sample7.dart index ee2f953a1..664e47477 100644 --- a/example/lib/presentation/samples/bar/bar_chart_sample7.dart +++ b/example/lib/presentation/samples/bar/bar_chart_sample7.dart @@ -230,7 +230,8 @@ class _IconWidgetState extends AnimatedWidgetBaseState<_IconWidget> { final rotation = math.pi * 4 * _rotationTween!.evaluate(animation); final scale = 1 + _rotationTween!.evaluate(animation) * 0.5; return Transform( - transform: Matrix4.rotationZ(rotation).scaledByDouble(scale, scale, scale, 1.0), + transform: + Matrix4.rotationZ(rotation).scaledByDouble(scale, scale, scale, 1.0), origin: const Offset(14, 14), child: Icon( widget.isSelected ? Icons.face_retouching_natural : Icons.face, diff --git a/example/lib/presentation/samples/line/line_chart_sample1.dart b/example/lib/presentation/samples/line/line_chart_sample1.dart index f9ad583df..2c260fa20 100644 --- a/example/lib/presentation/samples/line/line_chart_sample1.dart +++ b/example/lib/presentation/samples/line/line_chart_sample1.dart @@ -185,7 +185,7 @@ class _LineChart extends StatelessWidget { ); LineChartBarData get lineChartBarData1_1 => LineChartBarData( - isCurved: true, + curve: const LineChartCubicTensionCurve(), color: AppColors.contentColorGreen, barWidth: 8, isStrokeCapRound: true, @@ -203,7 +203,7 @@ class _LineChart extends StatelessWidget { ); LineChartBarData get lineChartBarData1_2 => LineChartBarData( - isCurved: true, + curve: const LineChartCubicTensionCurve(), color: AppColors.contentColorPink, barWidth: 8, isStrokeCapRound: true, @@ -223,7 +223,7 @@ class _LineChart extends StatelessWidget { ); LineChartBarData get lineChartBarData1_3 => LineChartBarData( - isCurved: true, + curve: const LineChartCubicTensionCurve(), color: AppColors.contentColorCyan, barWidth: 8, isStrokeCapRound: true, @@ -239,8 +239,7 @@ class _LineChart extends StatelessWidget { ); LineChartBarData get lineChartBarData2_1 => LineChartBarData( - isCurved: true, - curveSmoothness: 0, + curve: LineChartCurve.noCurve, color: AppColors.contentColorGreen.withValues(alpha: 0.5), barWidth: 4, isStrokeCapRound: true, @@ -258,7 +257,7 @@ class _LineChart extends StatelessWidget { ); LineChartBarData get lineChartBarData2_2 => LineChartBarData( - isCurved: true, + curve: const LineChartCubicTensionCurve(), color: AppColors.contentColorPink.withValues(alpha: 0.5), barWidth: 4, isStrokeCapRound: true, @@ -278,8 +277,7 @@ class _LineChart extends StatelessWidget { ); LineChartBarData get lineChartBarData2_3 => LineChartBarData( - isCurved: true, - curveSmoothness: 0, + curve: LineChartCurve.noCurve, color: AppColors.contentColorCyan.withValues(alpha: 0.5), barWidth: 2, isStrokeCapRound: true, diff --git a/example/lib/presentation/samples/line/line_chart_sample10.dart b/example/lib/presentation/samples/line/line_chart_sample10.dart index 5b1a28eb3..f216b07c5 100644 --- a/example/lib/presentation/samples/line/line_chart_sample10.dart +++ b/example/lib/presentation/samples/line/line_chart_sample10.dart @@ -119,7 +119,7 @@ class _LineChartSample10State extends State { stops: const [0.1, 1.0], ), barWidth: 4, - isCurved: false, + curve: LineChartCurve.noCurve, ); } @@ -134,7 +134,7 @@ class _LineChartSample10State extends State { stops: const [0.1, 1.0], ), barWidth: 4, - isCurved: false, + curve: LineChartCurve.noCurve, ); } diff --git a/example/lib/presentation/samples/line/line_chart_sample13.dart b/example/lib/presentation/samples/line/line_chart_sample13.dart index 0b4e5f62b..cb45c2471 100644 --- a/example/lib/presentation/samples/line/line_chart_sample13.dart +++ b/example/lib/presentation/samples/line/line_chart_sample13.dart @@ -172,7 +172,7 @@ class _LineChartSample13State extends State { ), ); }).toList(), - isCurved: false, + curve: LineChartCurve.noCurve, dotData: const FlDotData(show: false), color: AppColors.contentColorBlue, barWidth: 1, diff --git a/example/lib/presentation/samples/line/line_chart_sample2.dart b/example/lib/presentation/samples/line/line_chart_sample2.dart index a22e2b3de..3cbb117d7 100644 --- a/example/lib/presentation/samples/line/line_chart_sample2.dart +++ b/example/lib/presentation/samples/line/line_chart_sample2.dart @@ -173,7 +173,7 @@ class _LineChartSample2State extends State { FlSpot(9.5, 3), FlSpot(11, 4), ], - isCurved: true, + curve: const LineChartCubicTensionCurve(), gradient: LinearGradient( colors: gradientColors, ), @@ -260,7 +260,7 @@ class _LineChartSample2State extends State { FlSpot(9.5, 3.44), FlSpot(11, 3.44), ], - isCurved: true, + curve: const LineChartCubicTensionCurve(), gradient: LinearGradient( colors: [ ColorTween(begin: gradientColors[0], end: gradientColors[1]) diff --git a/example/lib/presentation/samples/line/line_chart_sample3.dart b/example/lib/presentation/samples/line/line_chart_sample3.dart index f79dbf915..53959ae32 100644 --- a/example/lib/presentation/samples/line/line_chart_sample3.dart +++ b/example/lib/presentation/samples/line/line_chart_sample3.dart @@ -299,7 +299,7 @@ class _LineChartSample3State extends State { spots: widget.yValues.asMap().entries.map((e) { return FlSpot(e.key.toDouble(), e.value); }).toList(), - isCurved: false, + curve: LineChartCurve.noCurve, barWidth: 4, color: widget.lineColor, belowBarData: BarAreaData( diff --git a/example/lib/presentation/samples/line/line_chart_sample4.dart b/example/lib/presentation/samples/line/line_chart_sample4.dart index b03ddd5e0..135a1c528 100644 --- a/example/lib/presentation/samples/line/line_chart_sample4.dart +++ b/example/lib/presentation/samples/line/line_chart_sample4.dart @@ -119,7 +119,7 @@ class LineChartSample4 extends StatelessWidget { FlSpot(10, 6), FlSpot(11, 7), ], - isCurved: true, + curve: const LineChartCubicTensionCurve(), barWidth: 8, color: mainLineColor, belowBarData: BarAreaData( diff --git a/example/lib/presentation/samples/line/line_chart_sample5.dart b/example/lib/presentation/samples/line/line_chart_sample5.dart index 61ff3b943..3ab124711 100644 --- a/example/lib/presentation/samples/line/line_chart_sample5.dart +++ b/example/lib/presentation/samples/line/line_chart_sample5.dart @@ -82,7 +82,7 @@ class _LineChartSample5State extends State { LineChartBarData( showingIndicators: showingTooltipOnSpots, spots: allSpots, - isCurved: true, + curve: const LineChartCubicTensionCurve(), barWidth: 4, shadow: const Shadow( blurRadius: 8, diff --git a/example/lib/presentation/samples/line/line_chart_sample6.dart b/example/lib/presentation/samples/line/line_chart_sample6.dart index 9f2f306ea..c9fd219e9 100644 --- a/example/lib/presentation/samples/line/line_chart_sample6.dart +++ b/example/lib/presentation/samples/line/line_chart_sample6.dart @@ -165,7 +165,7 @@ class LineChartSample6 extends StatelessWidget { ], ), spots: reverseSpots(spots, minSpotY, maxSpotY), - isCurved: true, + curve: const LineChartCubicTensionCurve(), isStrokeCapRound: true, barWidth: 10, belowBarData: BarAreaData( @@ -195,7 +195,7 @@ class LineChartSample6 extends StatelessWidget { ], ), spots: reverseSpots(spots2, minSpotY, maxSpotY), - isCurved: true, + curve: const LineChartCubicTensionCurve(), isStrokeCapRound: true, barWidth: 10, belowBarData: BarAreaData( diff --git a/example/lib/presentation/samples/line/line_chart_sample7.dart b/example/lib/presentation/samples/line/line_chart_sample7.dart index fa10b884e..b36110959 100644 --- a/example/lib/presentation/samples/line/line_chart_sample7.dart +++ b/example/lib/presentation/samples/line/line_chart_sample7.dart @@ -113,7 +113,7 @@ class LineChartSample7 extends StatelessWidget { FlSpot(10, 6), FlSpot(11, 7), ], - isCurved: true, + curve: const LineChartCubicTensionCurve(), barWidth: 2, color: line1Color, dotData: const FlDotData( @@ -135,7 +135,7 @@ class LineChartSample7 extends StatelessWidget { FlSpot(10, 1), FlSpot(11, 3), ], - isCurved: false, + curve: LineChartCurve.noCurve, barWidth: 2, color: line2Color, dotData: const FlDotData( diff --git a/example/lib/presentation/samples/line/line_chart_sample8.dart b/example/lib/presentation/samples/line/line_chart_sample8.dart index 0a0383b45..7e801c17e 100644 --- a/example/lib/presentation/samples/line/line_chart_sample8.dart +++ b/example/lib/presentation/samples/line/line_chart_sample8.dart @@ -277,7 +277,7 @@ class _LineChartSample8State extends State { FlSpot(11, 2.5), ], dashArray: [10, 6], - isCurved: true, + curve: const LineChartCubicTensionCurve(), color: AppColors.contentColorRed, barWidth: 4, isStrokeCapRound: true, diff --git a/example/lib/presentation/samples/line/line_chart_sample9.dart b/example/lib/presentation/samples/line/line_chart_sample9.dart index 7143fbd28..1dccfad56 100644 --- a/example/lib/presentation/samples/line/line_chart_sample9.dart +++ b/example/lib/presentation/samples/line/line_chart_sample9.dart @@ -82,7 +82,7 @@ class LineChartSample9 extends StatelessWidget { LineChartBarData( color: AppColors.contentColorPink, spots: spots, - isCurved: true, + curve: const LineChartCubicTensionCurve(), isStrokeCapRound: true, barWidth: 3, belowBarData: BarAreaData( diff --git a/lib/fl_chart.dart b/lib/fl_chart.dart index efc2e1e9e..c2b74eecc 100644 --- a/lib/fl_chart.dart +++ b/lib/fl_chart.dart @@ -9,6 +9,7 @@ export 'src/chart/base/base_chart/fl_touch_event.dart'; export 'src/chart/candlestick_chart/candlestick_chart.dart'; export 'src/chart/candlestick_chart/candlestick_chart_data.dart'; export 'src/chart/line_chart/line_chart.dart'; +export 'src/chart/line_chart/line_chart_curve.dart'; export 'src/chart/line_chart/line_chart_data.dart'; export 'src/chart/pie_chart/pie_chart.dart'; export 'src/chart/pie_chart/pie_chart_data.dart'; diff --git a/lib/src/chart/line_chart/line_chart_curve.dart b/lib/src/chart/line_chart/line_chart_curve.dart new file mode 100644 index 000000000..e76cae947 --- /dev/null +++ b/lib/src/chart/line_chart/line_chart_curve.dart @@ -0,0 +1,343 @@ +import 'dart:math'; +import 'dart:ui'; + +import 'package:equatable/equatable.dart'; + +LineChartCurve lerpCurve(LineChartCurve a, LineChartCurve b, double t) { + a = a._withNoCurveResolved; + b = b._withNoCurveResolved; + + if (a.runtimeType == b.runtimeType) { + return a.lerp(b, t) as LineChartCurve; + } + + return b; +} + +abstract class LineChartCurve> with EquatableMixin { + const LineChartCurve(); + + static const noCurve = LineChartNoCurve(); + + static LineChartCubicTensionCurve cubicTension({ + double smoothness = 0.35, + bool preventCurveOverShooting = false, + double preventCurveOvershootingThreshold = 10.0, + }) => + LineChartCubicTensionCurve( + smoothness: smoothness, + preventCurveOverShooting: preventCurveOverShooting, + preventCurveOvershootingThreshold: preventCurveOvershootingThreshold, + ); + + static LineChartCubicMonotoneCurve cubicMonotone({ + double smooth = 0.5, + SmoothMonotone monotone = SmoothMonotone.none, + double tinyThresholdSquared = 0.5, + }) => + LineChartCubicMonotoneCurve( + smooth: smooth, + monotone: monotone, + tinyThresholdSquared: tinyThresholdSquared, + ); + + /// Returns a linear-equivalent configuration of this curve for lerping. + /// When interpolating with a "no curve" (straight line), this should be an + /// instance of the same curve type configured to behave as a straight line. + /// If your curve becomes linear at a specific parameter value (e.g. smoothness = 0), + /// return that configuration; otherwise, return `this`. + /// + // ignore: avoid_returning_this + LineChartCurve get noCurveCase => this; + + /// Appends the segment from [previous] to [current] into the [path]. + /// Implementations may use [next] to compute control points for smoothing. + /// + /// Note: Iteration starts from the second data point. The first point is + /// already handled internally (e.g. moved to the path), so this method is + /// called beginning at index 1. When [current] is the last point, [next] + /// is `null`. + void appendToPath(Path path, Offset previous, Offset current, Offset? next); + + T lerp(covariant T other, double t); + + LineChartCurve get _withNoCurveResolved => + this is LineChartNoCurve ? noCurveCase : this; +} + +class LineChartNoCurve extends LineChartCurve { + const LineChartNoCurve(); + + @override + void appendToPath(Path path, Offset previous, Offset current, Offset? next) { + path.lineTo(current.dx, current.dy); + } + + @override + LineChartNoCurve lerp(LineChartNoCurve other, double t) => other; + + @override + List get props => const []; +} + +class LineChartCubicTensionCurve + extends LineChartCurve with EquatableMixin { + const LineChartCubicTensionCurve({ + this.smoothness = 0.35, + this.preventCurveOverShooting = false, + this.preventCurveOvershootingThreshold = 10.0, + }); + + /// It determines smoothness of the curved edges. + final double smoothness; + + /// Prevent overshooting when draw curve line with high value changes. + /// check this [issue](https://github.com/imaNNeo/fl_chart/issues/25) + final double preventCurveOvershootingThreshold; + + /// Applies threshold for [preventCurveOverShooting] algorithm. + final bool preventCurveOverShooting; + + @override + LineChartCubicTensionCurve get noCurveCase => LineChartCubicTensionCurve( + smoothness: 0, + preventCurveOverShooting: preventCurveOverShooting, + preventCurveOvershootingThreshold: preventCurveOvershootingThreshold, + ); + + static Offset _flag = Offset.zero; + + @override + void appendToPath(Path path, Offset previous, Offset current, Offset? next) { + final resolvedNext = next ?? current; + + final controlPoint1 = previous + _flag; + + _flag = ((resolvedNext - previous) / 2) * smoothness; + + if (preventCurveOverShooting) { + if ((resolvedNext - current).dy <= preventCurveOvershootingThreshold || + (current - previous).dy <= preventCurveOvershootingThreshold) { + _flag = Offset(_flag.dx, 0); + } + + if ((resolvedNext - current).dx <= preventCurveOvershootingThreshold || + (current - previous).dx <= preventCurveOvershootingThreshold) { + _flag = Offset(0, _flag.dy); + } + } + + final controlPoint2 = current - _flag; + + path.cubicTo( + controlPoint1.dx, + controlPoint1.dy, + controlPoint2.dx, + controlPoint2.dy, + current.dx, + current.dy, + ); + + // reset flag when have no next point + if (next == null) { + _flag = Offset.zero; + } + } + + @override + LineChartCubicTensionCurve lerp(LineChartCubicTensionCurve other, double t) => + LineChartCubicTensionCurve( + smoothness: lerpDouble(smoothness, other.smoothness, t)!, + preventCurveOverShooting: other.preventCurveOverShooting, + preventCurveOvershootingThreshold: lerpDouble( + preventCurveOvershootingThreshold, + other.preventCurveOvershootingThreshold, + t, + )!, + ); + + LineChartCubicTensionCurve copyWith({ + double? smoothness, + bool? preventCurveOverShooting, + double? preventCurveOvershootingThreshold, + }) => + LineChartCubicTensionCurve( + smoothness: smoothness ?? this.smoothness, + preventCurveOverShooting: + preventCurveOverShooting ?? this.preventCurveOverShooting, + preventCurveOvershootingThreshold: preventCurveOvershootingThreshold ?? + this.preventCurveOvershootingThreshold, + ); + + @override + List get props => [ + smoothness, + preventCurveOverShooting, + preventCurveOvershootingThreshold, + ]; +} + +enum SmoothMonotone { none, x, y } + +/// Source: +/// https://github.com/apache/echarts/blob/513918064ac2a0866433d434dc969220f12b9c1a/src/chart/line/poly.ts#L39 +/// Copied from the ECharts implementation. +class LineChartCubicMonotoneCurve + extends LineChartCurve { + const LineChartCubicMonotoneCurve({ + this.smooth = 0.5, + this.monotone = SmoothMonotone.none, + this.tinyThresholdSquared = 0.5, + }); + + /// Smoothing factor controlling how rounded the curve is. + /// 0 draws straight segments; 1 yields the roundest result. + /// Used to scale cubic control points between data vertices. + final double smooth; + + /// Optional monotonicity constraint along a single axis. + /// Keeps the curve from overshooting in the selected axis (`x` or `y`). + /// `none` uses the general length-weighted method without constraints. + final SmoothMonotone monotone; + + /// Squared distance threshold to treat adjacent points as identical. + /// If (dx^2 + dy^2) is below this, a straight line is drawn to avoid jitter. + /// Unit: logical pixels squared. + final double tinyThresholdSquared; + + @override + LineChartCubicMonotoneCurve get noCurveCase => LineChartCubicMonotoneCurve( + smooth: 0, + monotone: monotone, + tinyThresholdSquared: tinyThresholdSquared, + ); + + static Offset? _flag; + + @override + void appendToPath(Path path, Offset previous, Offset current, Offset? next) { + if (smooth <= 0) { + path.lineTo(current.dx, current.dy); + _flag = current; + return; + } + + final cp0Init = _flag ?? previous; + final cpx0 = cp0Init.dx; + final cpy0 = cp0Init.dy; + + final dx = current.dx - previous.dx; + final dy = current.dy - previous.dy; + if (dx * dx + dy * dy < tinyThresholdSquared) { + path.lineTo(current.dx, current.dy); + return; + } + + double cpx1; + double cpy1; + double nextCpx0; + double nextCpy0; + + if (next == null) { + cpx1 = current.dx; + cpy1 = current.dy; + nextCpx0 = current.dx; + nextCpy0 = current.dy; + } else { + var vx = next.dx - previous.dx; + var vy = next.dy - previous.dy; + final dx0 = current.dx - previous.dx; + final dy0 = current.dy - previous.dy; + final dx1 = next.dx - current.dx; + final dy1 = next.dy - current.dy; + + if (monotone == SmoothMonotone.x) { + final lenPrevSeg = dx0.abs(); + final lenNextSeg = dx1.abs(); + final dir = vx > 0 ? 1.0 : -1.0; + cpx1 = current.dx - dir * lenPrevSeg * smooth; + cpy1 = current.dy; + nextCpx0 = current.dx + dir * lenNextSeg * smooth; + nextCpy0 = current.dy; + } else if (monotone == SmoothMonotone.y) { + final lenPrevSeg = dy0.abs(); + final lenNextSeg = dy1.abs(); + final dir = vy > 0 ? 1.0 : -1.0; + cpx1 = current.dx; + cpy1 = current.dy - dir * lenPrevSeg * smooth; + nextCpx0 = current.dx; + nextCpy0 = current.dy + dir * lenNextSeg * smooth; + } else { + final lenPrevSeg = sqrt(dx0 * dx0 + dy0 * dy0); + final lenNextSeg = sqrt(dx1 * dx1 + dy1 * dy1); + if (lenPrevSeg == 0 || lenNextSeg == 0) { + path.lineTo(current.dx, current.dy); + _flag = current; + return; + } + + final ratioNextSeg = lenNextSeg / (lenNextSeg + lenPrevSeg); + + cpx1 = current.dx - vx * smooth * (1 - ratioNextSeg); + cpy1 = current.dy - vy * smooth * (1 - ratioNextSeg); + + nextCpx0 = current.dx + vx * smooth * ratioNextSeg; + nextCpy0 = current.dy + vy * smooth * ratioNextSeg; + + final double minX = min(next.dx, current.dx); + final double maxX = max(next.dx, current.dx); + final double minY = min(next.dy, current.dy); + final double maxY = max(next.dy, current.dy); + nextCpx0 = min(nextCpx0, maxX); + nextCpx0 = max(nextCpx0, minX); + nextCpy0 = min(nextCpy0, maxY); + nextCpy0 = max(nextCpy0, minY); + + vx = nextCpx0 - current.dx; + vy = nextCpy0 - current.dy; + cpx1 = current.dx - vx * lenPrevSeg / lenNextSeg; + cpy1 = current.dy - vy * lenPrevSeg / lenNextSeg; + + final double minPX = min(previous.dx, current.dx); + final double maxPX = max(previous.dx, current.dx); + final double minPY = min(previous.dy, current.dy); + final double maxPY = max(previous.dy, current.dy); + cpx1 = min(cpx1, maxPX); + cpx1 = max(cpx1, minPX); + cpy1 = min(cpy1, maxPY); + cpy1 = max(cpy1, minPY); + + final ax = current.dx - cpx1; + final ay = current.dy - cpy1; + nextCpx0 = current.dx + ax * lenNextSeg / lenPrevSeg; + nextCpy0 = current.dy + ay * lenNextSeg / lenPrevSeg; + } + } + + path.cubicTo(cpx0, cpy0, cpx1, cpy1, current.dx, current.dy); + + // reset flag when have no next point + if (next == null) { + _flag = null; + } else { + _flag = Offset(nextCpx0, nextCpy0); + } + } + + @override + LineChartCubicMonotoneCurve lerp( + covariant LineChartCubicMonotoneCurve other, double t) => + LineChartCubicMonotoneCurve( + smooth: lerpDouble(smooth, other.smooth, t)!, + monotone: other.monotone, + tinyThresholdSquared: + lerpDouble(tinyThresholdSquared, other.tinyThresholdSquared, t)!, + ); + + @override + List get props => [ + smooth, + monotone, + tinyThresholdSquared, + ]; +} diff --git a/lib/src/chart/line_chart/line_chart_data.dart b/lib/src/chart/line_chart/line_chart_data.dart index 22ee479f5..5c09fe392 100644 --- a/lib/src/chart/line_chart/line_chart_data.dart +++ b/lib/src/chart/line_chart/line_chart_data.dart @@ -198,8 +198,8 @@ class LineChartBarData with EquatableMixin { /// [BarChart] draws some lines and overlaps them in the chart's view, /// You can have multiple lines by splitting them, /// put a [FlSpot.nullSpot] between each section. - /// each line passes through [spots], with hard edges by default, - /// [isCurved] makes it curve for drawing, and [curveSmoothness] determines the curve smoothness. + /// each line passes through [spots], with hard edges by default. + /// Use [curve] to control how segments are drawn (straight or smoothed). /// /// [show] determines the drawing, if set to false, it draws nothing. /// @@ -243,10 +243,17 @@ class LineChartBarData with EquatableMixin { this.gradient, this.gradientArea = LineChartGradientArea.rectAroundTheLine, this.barWidth = 2.0, - this.isCurved = false, - this.curveSmoothness = 0.35, - this.preventCurveOverShooting = false, - this.preventCurveOvershootingThreshold = 10.0, + LineChartCurve? curve, + @Deprecated('Use curve instead') bool isCurved = false, + @Deprecated( + 'Use LineChartCubicTensionCurve.smoothness instead, or try other curve types') + double curveSmoothness = 0.35, + @Deprecated( + 'Use LineChartCubicTensionCurve.preventCurveOverShooting instead, or try other curve types') + bool preventCurveOverShooting = false, + @Deprecated( + 'Use LineChartCubicTensionCurve.preventCurveOvershootingThreshold instead, or try other curve types') + double preventCurveOvershootingThreshold = 10.0, this.isStrokeCapRound = false, this.isStrokeJoinRound = false, BarAreaData? belowBarData, @@ -262,7 +269,10 @@ class LineChartBarData with EquatableMixin { }) : color = color ?? ((color == null && gradient == null) ? Colors.cyan : null), belowBarData = belowBarData ?? BarAreaData(), - aboveBarData = aboveBarData ?? BarAreaData() { + aboveBarData = aboveBarData ?? BarAreaData(), + curve = curve ?? + _resovleCurve(isCurved, curveSmoothness, preventCurveOverShooting, + preventCurveOvershootingThreshold) { FlSpot? mostLeft; FlSpot? mostTop; FlSpot? mostRight; @@ -303,6 +313,20 @@ class LineChartBarData with EquatableMixin { } } + static LineChartCurve _resovleCurve( + bool isCurved, + double curveSmoothness, + bool preventCurveOverShooting, + double preventCurveOvershootingThreshold) => + isCurved + ? LineChartCubicTensionCurve( + smoothness: curveSmoothness, + preventCurveOverShooting: preventCurveOverShooting, + preventCurveOvershootingThreshold: + preventCurveOvershootingThreshold, + ) + : LineChartCurve.noCurve; + /// This line goes through this spots. /// /// You can have multiple lines by splitting them, @@ -342,19 +366,11 @@ class LineChartBarData with EquatableMixin { /// Determines thickness of drawing line. final double barWidth; - /// If it's true, [LineChart] draws the line with curved edges, - /// otherwise it draws line with hard edges. - final bool isCurved; - - /// If [isCurved] is true, it determines smoothness of the curved edges. - final double curveSmoothness; - - /// Prevent overshooting when draw curve line with high value changes. - /// check this [issue](https://github.com/imaNNeo/fl_chart/issues/25) - final bool preventCurveOverShooting; - - /// Applies threshold for [preventCurveOverShooting] algorithm. - final double preventCurveOvershootingThreshold; + /// Curve strategy for drawing segments between spots. + /// Use a built-in curve (e.g. [LineChartCurve.noCurve], + /// [LineChartCubicTensionCurve], [LineChartCubicMonotoneCurve]) + /// or provide your own implementation of [LineChartCurve]. + final LineChartCurve curve; /// Determines the style of line's cap. final bool isStrokeCapRound; @@ -401,16 +417,9 @@ class LineChartBarData with EquatableMixin { barWidth: lerpDouble(a.barWidth, b.barWidth, t)!, belowBarData: BarAreaData.lerp(a.belowBarData, b.belowBarData, t), aboveBarData: BarAreaData.lerp(a.aboveBarData, b.aboveBarData, t), - curveSmoothness: b.curveSmoothness, - isCurved: b.isCurved, isStrokeCapRound: b.isStrokeCapRound, isStrokeJoinRound: b.isStrokeJoinRound, - preventCurveOverShooting: b.preventCurveOverShooting, - preventCurveOvershootingThreshold: lerpDouble( - a.preventCurveOvershootingThreshold, - b.preventCurveOvershootingThreshold, - t, - )!, + curve: lerpCurve(a.curve, b.curve, t), dotData: FlDotData.lerp(a.dotData, b.dotData, t), errorIndicatorData: FlErrorIndicatorData.lerp( a.errorIndicatorData, @@ -438,9 +447,16 @@ class LineChartBarData with EquatableMixin { Gradient? gradient, LineChartGradientArea? gradientArea, double? barWidth, - bool? isCurved, + LineChartCurve? curve, + @Deprecated('Use curve instead') bool? isCurved, + @Deprecated( + 'Use LineChartCubicTensionCurve.smoothness instead, or try other curve types') double? curveSmoothness, + @Deprecated( + 'Use LineChartCubicTensionCurve.preventCurveOverShooting instead, or try other curve types') bool? preventCurveOverShooting, + @Deprecated( + 'Use LineChartCubicTensionCurve.preventCurveOvershootingThreshold instead, or try other curve types') double? preventCurveOvershootingThreshold, bool? isStrokeCapRound, bool? isStrokeJoinRound, @@ -462,12 +478,10 @@ class LineChartBarData with EquatableMixin { gradient: gradient ?? this.gradient, gradientArea: gradientArea ?? this.gradientArea, barWidth: barWidth ?? this.barWidth, - isCurved: isCurved ?? this.isCurved, - curveSmoothness: curveSmoothness ?? this.curveSmoothness, - preventCurveOverShooting: - preventCurveOverShooting ?? this.preventCurveOverShooting, - preventCurveOvershootingThreshold: preventCurveOvershootingThreshold ?? - this.preventCurveOvershootingThreshold, + curve: curve ?? + _resolveCopyWithCurve(isCurved, curveSmoothness, + preventCurveOverShooting, preventCurveOvershootingThreshold) ?? + this.curve, isStrokeCapRound: isStrokeCapRound ?? this.isStrokeCapRound, isStrokeJoinRound: isStrokeJoinRound ?? this.isStrokeJoinRound, belowBarData: belowBarData ?? this.belowBarData, @@ -481,6 +495,31 @@ class LineChartBarData with EquatableMixin { lineChartStepData: lineChartStepData ?? this.lineChartStepData, ); + LineChartCurve? _resolveCopyWithCurve( + bool? isCurved, + double? curveSmoothness, + bool? preventCurveOverShooting, + double? preventCurveOvershootingThreshold) { + if (isCurved == null) { + return switch (curve) { + final LineChartCubicTensionCurve cubicCurve => cubicCurve.copyWith( + smoothness: curveSmoothness, + preventCurveOverShooting: preventCurveOverShooting, + preventCurveOvershootingThreshold: + preventCurveOvershootingThreshold, + ), + final LineChartNoCurve noCurve => noCurve, + _ => null, + }; + } + + if (isCurved) { + return const LineChartCubicTensionCurve(); + } else { + return const LineChartNoCurve(); + } + } + /// Used for equality check, see [EquatableMixin]. @override List get props => [ @@ -490,10 +529,7 @@ class LineChartBarData with EquatableMixin { gradient, gradientArea, barWidth, - isCurved, - curveSmoothness, - preventCurveOverShooting, - preventCurveOvershootingThreshold, + curve, isStrokeCapRound, isStrokeJoinRound, belowBarData, diff --git a/lib/src/chart/line_chart/line_chart_painter.dart b/lib/src/chart/line_chart/line_chart_painter.dart index 6f47aee2d..0d626a659 100644 --- a/lib/src/chart/line_chart/line_chart_painter.dart +++ b/lib/src/chart/line_chart/line_chart_painter.dart @@ -572,8 +572,6 @@ class LineChartPainter extends AxisChartPainter { final path = appendToPath ?? Path(); final size = barSpots.length; - var temp = Offset.zero; - final x = getPixelX(barSpots[0].x, viewSize, holder); final y = getPixelY(barSpots[0].y, viewSize, holder); if (appendToPath == null) { @@ -598,43 +596,14 @@ class LineChartPainter extends AxisChartPainter { ); /// next point - final next = Offset( - getPixelX(barSpots[i + 1 < size ? i + 1 : i].x, viewSize, holder), - getPixelY(barSpots[i + 1 < size ? i + 1 : i].y, viewSize, holder), - ); - - final controlPoint1 = previous + temp; - - /// if the isCurved is false, we set 0 for smoothness, - /// it means we should not have any smoothness then we face with - /// the sharped corners line - final smoothness = barData.isCurved ? barData.curveSmoothness : 0.0; - temp = ((next - previous) / 2) * smoothness; - - if (barData.preventCurveOverShooting) { - if ((next - current).dy <= barData.preventCurveOvershootingThreshold || - (current - previous).dy <= - barData.preventCurveOvershootingThreshold) { - temp = Offset(temp.dx, 0); - } - - if ((next - current).dx <= barData.preventCurveOvershootingThreshold || - (current - previous).dx <= - barData.preventCurveOvershootingThreshold) { - temp = Offset(0, temp.dy); - } - } - - final controlPoint2 = current - temp; + final next = i < size - 1 + ? Offset( + getPixelX(barSpots[i + 1].x, viewSize, holder), + getPixelY(barSpots[i + 1].y, viewSize, holder), + ) + : null; - path.cubicTo( - controlPoint1.dx, - controlPoint1.dy, - controlPoint2.dx, - controlPoint2.dy, - current.dx, - current.dy, - ); + barData.curve.appendToPath(path, previous, current, next); } return path; diff --git a/test/chart/data_pool.dart b/test/chart/data_pool.dart index a32c8a5c3..e3b8e8c6e 100644 --- a/test/chart/data_pool.dart +++ b/test/chart/data_pool.dart @@ -147,10 +147,12 @@ class MockData { aboveBarData: barAreaData1, belowBarData: barAreaData2, barWidth: 12, - curveSmoothness: 12, + curve: const LineChartCubicTensionCurve( + smoothness: 12, + preventCurveOvershootingThreshold: 1.2, + ), dotData: flDotData1, isStrokeCapRound: true, - preventCurveOvershootingThreshold: 1.2, showingIndicators: [0, 1], ); @@ -171,10 +173,12 @@ class MockData { aboveBarData: barAreaData1, belowBarData: barAreaData2, barWidth: 12, - curveSmoothness: 12, + curve: const LineChartCubicTensionCurve( + smoothness: 12, + preventCurveOvershootingThreshold: 1.2, + ), dotData: flDotData1, isStrokeCapRound: true, - preventCurveOvershootingThreshold: 1.2, showingIndicators: [0, 4], ); @@ -829,10 +833,12 @@ final LineChartBarData lineChartBarData1 = LineChartBarData( aboveBarData: barAreaData1, belowBarData: barAreaData2, barWidth: 12, - curveSmoothness: 12, + curve: const LineChartCubicTensionCurve( + smoothness: 12, + preventCurveOvershootingThreshold: 1.2, + ), dotData: flDotData1, isStrokeCapRound: true, - preventCurveOvershootingThreshold: 1.2, showingIndicators: [0, 1], errorIndicatorData: const FlErrorIndicatorData( show: false, @@ -854,10 +860,12 @@ final LineChartBarData lineChartBarData1Clone = LineChartBarData( aboveBarData: barAreaData1Clone, belowBarData: barAreaData2, barWidth: 12, - curveSmoothness: 12, + curve: const LineChartCubicTensionCurve( + smoothness: 12, + preventCurveOvershootingThreshold: 1.2, + ), dotData: flDotData1Clone, isStrokeCapRound: true, - preventCurveOvershootingThreshold: 1.2, showingIndicators: [0, 1], errorIndicatorData: const FlErrorIndicatorData( show: false, @@ -881,10 +889,12 @@ final LineChartBarData lineChartBarData2 = LineChartBarData( aboveBarData: barAreaData1, belowBarData: barAreaData2, barWidth: 12, - curveSmoothness: 12, + curve: const LineChartCubicTensionCurve( + smoothness: 12, + preventCurveOvershootingThreshold: 1.2, + ), dotData: flDotData1, isStrokeCapRound: true, - preventCurveOvershootingThreshold: 1.2, showingIndicators: [0, 4], ); @@ -905,10 +915,12 @@ final LineChartBarData lineChartBarData3 = LineChartBarData( aboveBarData: barAreaData1, belowBarData: barAreaData2, barWidth: 12, - curveSmoothness: 12, + curve: const LineChartCubicTensionCurve( + smoothness: 12, + preventCurveOvershootingThreshold: 1.2, + ), dotData: flDotData1, isStrokeCapRound: true, - preventCurveOvershootingThreshold: 1.2, showingIndicators: [0, 1], ); @@ -929,10 +941,12 @@ final LineChartBarData lineChartBarData4 = LineChartBarData( aboveBarData: barAreaData1, belowBarData: barAreaData2, barWidth: 12, - curveSmoothness: 12, + curve: const LineChartCubicTensionCurve( + smoothness: 12, + preventCurveOvershootingThreshold: 1.2, + ), dotData: flDotData1, isStrokeCapRound: true, - preventCurveOvershootingThreshold: 1.2, showingIndicators: [0, 1], ); @@ -951,10 +965,12 @@ final LineChartBarData lineChartBarData5 = LineChartBarData( aboveBarData: barAreaData2, belowBarData: barAreaData1, barWidth: 12, - curveSmoothness: 12, + curve: const LineChartCubicTensionCurve( + smoothness: 12, + preventCurveOvershootingThreshold: 1.2, + ), dotData: flDotData1, isStrokeCapRound: true, - preventCurveOvershootingThreshold: 1.2, showingIndicators: [0, 1], ); @@ -973,10 +989,12 @@ final LineChartBarData lineChartBarData6 = LineChartBarData( aboveBarData: barAreaData1, belowBarData: barAreaData2, barWidth: 12, - curveSmoothness: 12, + curve: const LineChartCubicTensionCurve( + smoothness: 12, + preventCurveOvershootingThreshold: 1.2, + ), dotData: flDotData1, isStrokeCapRound: true, - preventCurveOvershootingThreshold: 1.2, showingIndicators: [0, 1], ); @@ -995,10 +1013,12 @@ final LineChartBarData lineChartBarData7 = LineChartBarData( aboveBarData: barAreaData1, belowBarData: barAreaData2, barWidth: 12, - curveSmoothness: 12, + curve: const LineChartCubicTensionCurve( + smoothness: 12, + preventCurveOvershootingThreshold: 1.2, + ), dotData: flDotData1, isStrokeCapRound: true, - preventCurveOvershootingThreshold: 1.2, showingIndicators: [0, 1], ); @@ -1017,10 +1037,12 @@ final LineChartBarData lineChartBarData8 = LineChartBarData( aboveBarData: barAreaData1, belowBarData: barAreaData2, barWidth: 12, - curveSmoothness: 12.01, + curve: const LineChartCubicTensionCurve( + smoothness: 12.01, + preventCurveOvershootingThreshold: 1.2, + ), dotData: flDotData1, isStrokeCapRound: true, - preventCurveOvershootingThreshold: 1.2, showingIndicators: [0, 1], ); @@ -1039,11 +1061,13 @@ final LineChartBarData lineChartBarData9 = LineChartBarData( aboveBarData: barAreaData1, belowBarData: barAreaData2, barWidth: 12, - curveSmoothness: 12, + curve: const LineChartCubicTensionCurve( + smoothness: 12, + preventCurveOverShooting: true, + preventCurveOvershootingThreshold: 1.2, + ), dotData: flDotData1, isStrokeCapRound: true, - preventCurveOverShooting: true, - preventCurveOvershootingThreshold: 1.2, showingIndicators: [0, 1], ); From 093c680a52d60814a926cf56f764e94ceeede3c6 Mon Sep 17 00:00:00 2001 From: hhh <1224991097@qq.com> Date: Mon, 27 Oct 2025 15:20:57 +0800 Subject: [PATCH 02/10] Update example --- example/lib/presentation/samples/line/line_chart_sample1.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/example/lib/presentation/samples/line/line_chart_sample1.dart b/example/lib/presentation/samples/line/line_chart_sample1.dart index 2c260fa20..6d7333ce2 100644 --- a/example/lib/presentation/samples/line/line_chart_sample1.dart +++ b/example/lib/presentation/samples/line/line_chart_sample1.dart @@ -223,7 +223,7 @@ class _LineChart extends StatelessWidget { ); LineChartBarData get lineChartBarData1_3 => LineChartBarData( - curve: const LineChartCubicTensionCurve(), + curve: const LineChartCubicMonotoneCurve(), color: AppColors.contentColorCyan, barWidth: 8, isStrokeCapRound: true, From 43efb689340293bd6af6ca3b90367121d091b308 Mon Sep 17 00:00:00 2001 From: hhh <1224991097@qq.com> Date: Mon, 27 Oct 2025 15:21:20 +0800 Subject: [PATCH 03/10] fix `lerpCurve` --- lib/src/chart/line_chart/line_chart_curve.dart | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/lib/src/chart/line_chart/line_chart_curve.dart b/lib/src/chart/line_chart/line_chart_curve.dart index e76cae947..92882d9ee 100644 --- a/lib/src/chart/line_chart/line_chart_curve.dart +++ b/lib/src/chart/line_chart/line_chart_curve.dart @@ -4,8 +4,12 @@ import 'dart:ui'; import 'package:equatable/equatable.dart'; LineChartCurve lerpCurve(LineChartCurve a, LineChartCurve b, double t) { - a = a._withNoCurveResolved; - b = b._withNoCurveResolved; + // Align curve types + (a, b) = switch ((a, b)) { + (final LineChartNoCurve _, _) => (b.noCurveCase, b), + (_, final LineChartNoCurve _) => (a, a.noCurveCase), + (_, _) => (a, b), + }; if (a.runtimeType == b.runtimeType) { return a.lerp(b, t) as LineChartCurve; @@ -60,9 +64,6 @@ abstract class LineChartCurve> with EquatableMixin { void appendToPath(Path path, Offset previous, Offset current, Offset? next); T lerp(covariant T other, double t); - - LineChartCurve get _withNoCurveResolved => - this is LineChartNoCurve ? noCurveCase : this; } class LineChartNoCurve extends LineChartCurve { From ec6473c34e35eb08c0ebccc4dda44549a25a38e1 Mon Sep 17 00:00:00 2001 From: hhh <1224991097@qq.com> Date: Mon, 27 Oct 2025 15:21:36 +0800 Subject: [PATCH 04/10] add some test --- .../line_chart/line_chart_curve_test.dart | 64 +++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 test/chart/line_chart/line_chart_curve_test.dart diff --git a/test/chart/line_chart/line_chart_curve_test.dart b/test/chart/line_chart/line_chart_curve_test.dart new file mode 100644 index 000000000..6cca93ed9 --- /dev/null +++ b/test/chart/line_chart/line_chart_curve_test.dart @@ -0,0 +1,64 @@ +import 'dart:ui'; + +import 'package:fl_chart/fl_chart.dart'; +import 'package:fl_chart/src/chart/line_chart/line_chart_curve.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void expectLikeStraightLine(LineChartCurve curve, + {Offset point0 = Offset.zero, Offset point1 = const Offset(10, 10)}) { + final path = Path()..moveTo(point0.dx, point0.dy); + curve.appendToPath(path, point0, point1, null); + + final metrics = path.computeMetrics().toList(); + expect(metrics.length, 1); + expect(metrics.single.length, closeTo(point1.distanceTo(point0), 0.001)); +} + +void main() { + group('LineChartNoCurve', () { + test('appendToPath draws straight line', () { + expectLikeStraightLine(const LineChartNoCurve()); + }); + + test('equality check', () { + const a = LineChartNoCurve(); + const b = LineChartNoCurve(); + + expect(a, equals(b)); + expect(a.hashCode, equals(b.hashCode)); + }); + }); + + group('LineChartCubicTensionCurve', () { + test('CubicTensionCurve with smoothness = 0 behaves like straight line', + () { + expectLikeStraightLine(LineChartCurve.cubicTension(smoothness: 0)); + }); + + test('equality check', () { + final a = LineChartCurve.cubicTension(); + final b = LineChartCurve.cubicTension(); + + expect(a, equals(b)); + expect(a.hashCode, equals(b.hashCode)); + }); + }); + + group('LineChartCubicMonotoneCurve', () { + test('CubicMonotoneCurve with smooth = 0 behaves like straight line', () { + expectLikeStraightLine(LineChartCurve.cubicMonotone(smooth: 0)); + }); + + test('equality check', () { + final a = LineChartCurve.cubicMonotone(); + final b = LineChartCurve.cubicMonotone(); + + expect(a, equals(b)); + expect(a.hashCode, equals(b.hashCode)); + }); + }); +} + +extension on Offset { + double distanceTo(Offset other) => (this - other).distance; +} From 7a882fa663259e8ff35aad0dad920b6f3301884d Mon Sep 17 00:00:00 2001 From: hhh <1224991097@qq.com> Date: Mon, 27 Oct 2025 15:37:39 +0800 Subject: [PATCH 05/10] code format --- .../chart/line_chart/line_chart_curve.dart | 4 +- lib/src/chart/line_chart/line_chart_data.dart | 52 ++++++++++++------- .../line_chart/line_chart_curve_test.dart | 7 ++- 3 files changed, 42 insertions(+), 21 deletions(-) diff --git a/lib/src/chart/line_chart/line_chart_curve.dart b/lib/src/chart/line_chart/line_chart_curve.dart index 92882d9ee..7a98c7e83 100644 --- a/lib/src/chart/line_chart/line_chart_curve.dart +++ b/lib/src/chart/line_chart/line_chart_curve.dart @@ -327,7 +327,9 @@ class LineChartCubicMonotoneCurve @override LineChartCubicMonotoneCurve lerp( - covariant LineChartCubicMonotoneCurve other, double t) => + covariant LineChartCubicMonotoneCurve other, + double t, + ) => LineChartCubicMonotoneCurve( smooth: lerpDouble(smooth, other.smooth, t)!, monotone: other.monotone, diff --git a/lib/src/chart/line_chart/line_chart_data.dart b/lib/src/chart/line_chart/line_chart_data.dart index 5c09fe392..13ca3c097 100644 --- a/lib/src/chart/line_chart/line_chart_data.dart +++ b/lib/src/chart/line_chart/line_chart_data.dart @@ -246,13 +246,16 @@ class LineChartBarData with EquatableMixin { LineChartCurve? curve, @Deprecated('Use curve instead') bool isCurved = false, @Deprecated( - 'Use LineChartCubicTensionCurve.smoothness instead, or try other curve types') + 'Use LineChartCubicTensionCurve.smoothness instead, or try other curve types', + ) double curveSmoothness = 0.35, @Deprecated( - 'Use LineChartCubicTensionCurve.preventCurveOverShooting instead, or try other curve types') + 'Use LineChartCubicTensionCurve.preventCurveOverShooting instead, or try other curve types', + ) bool preventCurveOverShooting = false, @Deprecated( - 'Use LineChartCubicTensionCurve.preventCurveOvershootingThreshold instead, or try other curve types') + 'Use LineChartCubicTensionCurve.preventCurveOvershootingThreshold instead, or try other curve types', + ) double preventCurveOvershootingThreshold = 10.0, this.isStrokeCapRound = false, this.isStrokeJoinRound = false, @@ -271,8 +274,12 @@ class LineChartBarData with EquatableMixin { belowBarData = belowBarData ?? BarAreaData(), aboveBarData = aboveBarData ?? BarAreaData(), curve = curve ?? - _resovleCurve(isCurved, curveSmoothness, preventCurveOverShooting, - preventCurveOvershootingThreshold) { + _resovleCurve( + isCurved, + curveSmoothness, + preventCurveOverShooting, + preventCurveOvershootingThreshold, + ) { FlSpot? mostLeft; FlSpot? mostTop; FlSpot? mostRight; @@ -314,10 +321,11 @@ class LineChartBarData with EquatableMixin { } static LineChartCurve _resovleCurve( - bool isCurved, - double curveSmoothness, - bool preventCurveOverShooting, - double preventCurveOvershootingThreshold) => + bool isCurved, + double curveSmoothness, + bool preventCurveOverShooting, + double preventCurveOvershootingThreshold, + ) => isCurved ? LineChartCubicTensionCurve( smoothness: curveSmoothness, @@ -450,13 +458,16 @@ class LineChartBarData with EquatableMixin { LineChartCurve? curve, @Deprecated('Use curve instead') bool? isCurved, @Deprecated( - 'Use LineChartCubicTensionCurve.smoothness instead, or try other curve types') + 'Use LineChartCubicTensionCurve.smoothness instead, or try other curve types', + ) double? curveSmoothness, @Deprecated( - 'Use LineChartCubicTensionCurve.preventCurveOverShooting instead, or try other curve types') + 'Use LineChartCubicTensionCurve.preventCurveOverShooting instead, or try other curve types', + ) bool? preventCurveOverShooting, @Deprecated( - 'Use LineChartCubicTensionCurve.preventCurveOvershootingThreshold instead, or try other curve types') + 'Use LineChartCubicTensionCurve.preventCurveOvershootingThreshold instead, or try other curve types', + ) double? preventCurveOvershootingThreshold, bool? isStrokeCapRound, bool? isStrokeJoinRound, @@ -479,8 +490,12 @@ class LineChartBarData with EquatableMixin { gradientArea: gradientArea ?? this.gradientArea, barWidth: barWidth ?? this.barWidth, curve: curve ?? - _resolveCopyWithCurve(isCurved, curveSmoothness, - preventCurveOverShooting, preventCurveOvershootingThreshold) ?? + _resolveCopyWithCurve( + isCurved, + curveSmoothness, + preventCurveOverShooting, + preventCurveOvershootingThreshold, + ) ?? this.curve, isStrokeCapRound: isStrokeCapRound ?? this.isStrokeCapRound, isStrokeJoinRound: isStrokeJoinRound ?? this.isStrokeJoinRound, @@ -496,10 +511,11 @@ class LineChartBarData with EquatableMixin { ); LineChartCurve? _resolveCopyWithCurve( - bool? isCurved, - double? curveSmoothness, - bool? preventCurveOverShooting, - double? preventCurveOvershootingThreshold) { + bool? isCurved, + double? curveSmoothness, + bool? preventCurveOverShooting, + double? preventCurveOvershootingThreshold, + ) { if (isCurved == null) { return switch (curve) { final LineChartCubicTensionCurve cubicCurve => cubicCurve.copyWith( diff --git a/test/chart/line_chart/line_chart_curve_test.dart b/test/chart/line_chart/line_chart_curve_test.dart index 6cca93ed9..67ada1770 100644 --- a/test/chart/line_chart/line_chart_curve_test.dart +++ b/test/chart/line_chart/line_chart_curve_test.dart @@ -4,8 +4,11 @@ import 'package:fl_chart/fl_chart.dart'; import 'package:fl_chart/src/chart/line_chart/line_chart_curve.dart'; import 'package:flutter_test/flutter_test.dart'; -void expectLikeStraightLine(LineChartCurve curve, - {Offset point0 = Offset.zero, Offset point1 = const Offset(10, 10)}) { +void expectLikeStraightLine( + LineChartCurve curve, { + Offset point0 = Offset.zero, + Offset point1 = const Offset(10, 10), +}) { final path = Path()..moveTo(point0.dx, point0.dy); curve.appendToPath(path, point0, point1, null); From 3c007076ad8839ee9441ebec9a8a58546722ac0f Mon Sep 17 00:00:00 2001 From: hhh <1224991097@qq.com> Date: Mon, 27 Oct 2025 16:28:06 +0800 Subject: [PATCH 06/10] add test for LineChartCubicMonotoneCurve smooth parameter behavior --- .../line_chart/line_chart_curve_test.dart | 180 ++++++++++++++++-- 1 file changed, 164 insertions(+), 16 deletions(-) diff --git a/test/chart/line_chart/line_chart_curve_test.dart b/test/chart/line_chart/line_chart_curve_test.dart index 67ada1770..e09b8843c 100644 --- a/test/chart/line_chart/line_chart_curve_test.dart +++ b/test/chart/line_chart/line_chart_curve_test.dart @@ -19,10 +19,6 @@ void expectLikeStraightLine( void main() { group('LineChartNoCurve', () { - test('appendToPath draws straight line', () { - expectLikeStraightLine(const LineChartNoCurve()); - }); - test('equality check', () { const a = LineChartNoCurve(); const b = LineChartNoCurve(); @@ -30,38 +26,190 @@ void main() { expect(a, equals(b)); expect(a.hashCode, equals(b.hashCode)); }); + + test('no curve case returns self', () { + const curve = LineChartNoCurve(); + expect(curve.noCurveCase, equals(curve)); + }); + + test('lerp with no curve case returns self', () { + const curve = LineChartNoCurve(); + expect(curve.lerp(curve, 0.5), equals(curve)); + }); + + test('appendToPath draws straight line', () { + expectLikeStraightLine(const LineChartNoCurve()); + }); }); group('LineChartCubicTensionCurve', () { + test('equality check', () { + const a = LineChartCubicTensionCurve(); + const b = LineChartCubicTensionCurve(); + + expect(a, equals(b)); + expect(a.hashCode, equals(b.hashCode)); + }); + + test('no curve case', () { + const curve = LineChartCubicTensionCurve(); + + expect( + curve.noCurveCase, + equals( + LineChartCubicTensionCurve( + smoothness: 0, + preventCurveOverShooting: curve.preventCurveOverShooting, + preventCurveOvershootingThreshold: + curve.preventCurveOvershootingThreshold, + ), + ), + ); + }); + test('CubicTensionCurve with smoothness = 0 behaves like straight line', () { - expectLikeStraightLine(LineChartCurve.cubicTension(smoothness: 0)); + expectLikeStraightLine(const LineChartCubicTensionCurve(smoothness: 0)); }); + }); + group('LineChartCubicMonotoneCurve', () { test('equality check', () { - final a = LineChartCurve.cubicTension(); - final b = LineChartCurve.cubicTension(); + const a = LineChartCubicMonotoneCurve(); + const b = LineChartCubicMonotoneCurve(); expect(a, equals(b)); expect(a.hashCode, equals(b.hashCode)); }); - }); - group('LineChartCubicMonotoneCurve', () { - test('CubicMonotoneCurve with smooth = 0 behaves like straight line', () { - expectLikeStraightLine(LineChartCurve.cubicMonotone(smooth: 0)); + test('no curve case', () { + const curve = LineChartCubicMonotoneCurve(); + + expect( + curve.noCurveCase, + equals( + LineChartCubicMonotoneCurve( + smooth: 0, + monotone: curve.monotone, + tinyThresholdSquared: curve.tinyThresholdSquared, + ), + ), + ); }); - test('equality check', () { - final a = LineChartCurve.cubicMonotone(); - final b = LineChartCurve.cubicMonotone(); + group('smooth parameter behavior', () { + test('smooth = 0 produces straight line', () { + expectLikeStraightLine( + const LineChartCubicMonotoneCurve(smooth: 0), + ); + }); - expect(a, equals(b)); - expect(a.hashCode, equals(b.hashCode)); + test('smooth = 0.3 produces curved line shorter than smooth = 0.7', () { + final points = [ + Offset.zero, + const Offset(10, 10), + const Offset(20, 5), + const Offset(30, 15), + ]; + + final path1 = _buildPathWithCurve( + points, + const LineChartCubicMonotoneCurve(smooth: 0.3), + ); + final path2 = _buildPathWithCurve( + points, + const LineChartCubicMonotoneCurve(smooth: 0.7), + ); + + final metrics1 = path1.computeMetrics().toList(); + final metrics2 = path2.computeMetrics().toList(); + + expect(metrics1.length, 1); + expect(metrics2.length, 1); + + // Higher smooth value should produce longer curved path + expect( + metrics2.single.length, + greaterThan(metrics1.single.length), + ); + }); + + test('smooth = 1.0 produces maximum smoothness', () { + final points = [ + Offset.zero, + const Offset(10, 10), + const Offset(20, 5), + ]; + + final path = _buildPathWithCurve( + points, + const LineChartCubicMonotoneCurve(smooth: 1), + ); + + final metrics = path.computeMetrics().toList(); + expect(metrics.length, 1); + // Curved path should be longer than straight line + expect( + metrics.single.length, + greaterThan( + points[0].distanceTo(points[1]) + points[1].distanceTo(points[2]), + ), + ); + }); + }); + + group('state management (_flag)', () { + test('flag resets after last point', () { + final points = [ + Offset.zero, + const Offset(10, 10), + const Offset(20, 5), + ]; + + // First path + final path1 = _buildPathWithCurve( + points, + const LineChartCubicMonotoneCurve(), + ); + + // Second path should not be affected by first path's flag + final path2 = _buildPathWithCurve( + points, + const LineChartCubicMonotoneCurve(), + ); + + final metrics1 = path1.computeMetrics().toList(); + final metrics2 = path2.computeMetrics().toList(); + + // Both paths should have identical length + expect( + metrics1.single.length, + closeTo(metrics2.single.length, 0.001), + ); + }); }); }); } +/// Helper function to build a complete path with the given curve +Path _buildPathWithCurve(List points, LineChartCurve curve) { + if (points.isEmpty) { + return Path(); + } + + final path = Path()..moveTo(points[0].dx, points[0].dy); + + for (var i = 1; i < points.length; i++) { + final previous = points[i - 1]; + final current = points[i]; + final next = i < points.length - 1 ? points[i + 1] : null; + + curve.appendToPath(path, previous, current, next); + } + + return path; +} + extension on Offset { double distanceTo(Offset other) => (this - other).distance; } From af2b7a893657b1ba532ecddce1e17bf4cf6f86ff Mon Sep 17 00:00:00 2001 From: hhh <1224991097@qq.com> Date: Mon, 27 Oct 2025 19:12:59 +0800 Subject: [PATCH 07/10] update test --- .../line_chart/line_chart_curve_test.dart | 243 ++++++++++-------- 1 file changed, 134 insertions(+), 109 deletions(-) diff --git a/test/chart/line_chart/line_chart_curve_test.dart b/test/chart/line_chart/line_chart_curve_test.dart index e09b8843c..3c58f7c4a 100644 --- a/test/chart/line_chart/line_chart_curve_test.dart +++ b/test/chart/line_chart/line_chart_curve_test.dart @@ -4,20 +4,26 @@ import 'package:fl_chart/fl_chart.dart'; import 'package:fl_chart/src/chart/line_chart/line_chart_curve.dart'; import 'package:flutter_test/flutter_test.dart'; -void expectLikeStraightLine( - LineChartCurve curve, { - Offset point0 = Offset.zero, - Offset point1 = const Offset(10, 10), -}) { - final path = Path()..moveTo(point0.dx, point0.dy); - curve.appendToPath(path, point0, point1, null); - - final metrics = path.computeMetrics().toList(); - expect(metrics.length, 1); - expect(metrics.single.length, closeTo(point1.distanceTo(point0), 0.001)); -} +const samplePoints1 = [ + Offset(10, 10), + Offset(20, 20), + Offset(30, 40), + Offset(40, 10), +]; void main() { + test('static constructor', () { + expect(LineChartCurve.noCurve, equals(const LineChartNoCurve())); + expect( + LineChartCurve.cubicTension(), + equals(const LineChartCubicTensionCurve()), + ); + expect( + LineChartCurve.cubicMonotone(), + equals(const LineChartCubicMonotoneCurve()), + ); + }); + group('LineChartNoCurve', () { test('equality check', () { const a = LineChartNoCurve(); @@ -51,19 +57,12 @@ void main() { expect(a.hashCode, equals(b.hashCode)); }); - test('no curve case', () { - const curve = LineChartCubicTensionCurve(); + test('lerp no curve', () { + const curve = LineChartCubicTensionCurve(smoothness: 0.8); expect( - curve.noCurveCase, - equals( - LineChartCubicTensionCurve( - smoothness: 0, - preventCurveOverShooting: curve.preventCurveOverShooting, - preventCurveOvershootingThreshold: - curve.preventCurveOvershootingThreshold, - ), - ), + lerpCurve(curve, const LineChartNoCurve(), 0.5), + equals(const LineChartCubicTensionCurve(smoothness: 0.4)), ); }); @@ -71,6 +70,46 @@ void main() { () { expectLikeStraightLine(const LineChartCubicTensionCurve(smoothness: 0)); }); + + test( + 'prevents overshoot when dy difference is below threshold in y direction', + () { + const curve = LineChartCubicTensionCurve( + preventCurveOverShooting: true, + ); + + // Create points where dy difference is below threshold (5 < 10) + final points = [ + Offset.zero, + const Offset(20, 5), // dy = 5 + const Offset(40, 8), // dy = 3 + ]; + + final path = _buildPathWithCurve(points, curve); + + final metrics = path.computeMetrics().toList(); + expect(metrics.length, 1); + }); + + test( + 'prevents overshoot when dx difference is below threshold in x direction', + () { + const curve = LineChartCubicTensionCurve( + preventCurveOverShooting: true, + ); + + // Create points where dx difference is below threshold (5 < 10) + final points = [ + Offset.zero, + const Offset(5, 20), // dx = 5 + const Offset(8, 40), // dx = 3 + ]; + + final path = _buildPathWithCurve(points, curve); + + final metrics = path.computeMetrics().toList(); + expect(metrics.length, 1); + }); }); group('LineChartCubicMonotoneCurve', () { @@ -82,122 +121,98 @@ void main() { expect(a.hashCode, equals(b.hashCode)); }); - test('no curve case', () { - const curve = LineChartCubicMonotoneCurve(); + test('lerp no curve', () { + const curve = LineChartCubicMonotoneCurve(smooth: 0.8); expect( - curve.noCurveCase, - equals( - LineChartCubicMonotoneCurve( - smooth: 0, - monotone: curve.monotone, - tinyThresholdSquared: curve.tinyThresholdSquared, - ), - ), + lerpCurve(const LineChartNoCurve(), curve, 0.5), + equals(const LineChartCubicMonotoneCurve(smooth: 0.4)), ); }); - group('smooth parameter behavior', () { - test('smooth = 0 produces straight line', () { - expectLikeStraightLine( - const LineChartCubicMonotoneCurve(smooth: 0), - ); - }); - - test('smooth = 0.3 produces curved line shorter than smooth = 0.7', () { - final points = [ - Offset.zero, - const Offset(10, 10), - const Offset(20, 5), - const Offset(30, 15), - ]; - - final path1 = _buildPathWithCurve( - points, - const LineChartCubicMonotoneCurve(smooth: 0.3), - ); - final path2 = _buildPathWithCurve( - points, - const LineChartCubicMonotoneCurve(smooth: 0.7), - ); - - final metrics1 = path1.computeMetrics().toList(); - final metrics2 = path2.computeMetrics().toList(); + test('draw straight line if just two points', () { + expectLikeStraightLine( + const LineChartCubicMonotoneCurve(tinyThresholdSquared: 0), + points: [Offset.zero, const Offset(10, 10)], + ); + }); - expect(metrics1.length, 1); - expect(metrics2.length, 1); + test('draw straight line if smooth = 0', () { + expectLikeStraightLine( + const LineChartCubicMonotoneCurve( + smooth: 0, + tinyThresholdSquared: double.infinity, + ), + ); + }); - // Higher smooth value should produce longer curved path - expect( - metrics2.single.length, - greaterThan(metrics1.single.length), + group('tinyThreshold parameter behavior', () { + test('draw straight line if distance < tinyThreshold', () { + expectLikeStraightLine( + const LineChartCubicMonotoneCurve( + tinyThresholdSquared: double.infinity, + ), ); }); - test('smooth = 1.0 produces maximum smoothness', () { - final points = [ - Offset.zero, - const Offset(10, 10), - const Offset(20, 5), - ]; - + test('draw curved line if distance > tinyThreshold', () { final path = _buildPathWithCurve( - points, - const LineChartCubicMonotoneCurve(smooth: 1), + samplePoints1, + const LineChartCubicMonotoneCurve(tinyThresholdSquared: 0), ); final metrics = path.computeMetrics().toList(); - expect(metrics.length, 1); - // Curved path should be longer than straight line + expect( metrics.single.length, - greaterThan( - points[0].distanceTo(points[1]) + points[1].distanceTo(points[2]), - ), + greaterThan(samplePoints1.straightDistance), ); }); }); - group('state management (_flag)', () { - test('flag resets after last point', () { - final points = [ - Offset.zero, - const Offset(10, 10), - const Offset(20, 5), - ]; - - // First path - final path1 = _buildPathWithCurve( - points, - const LineChartCubicMonotoneCurve(), - ); - - // Second path should not be affected by first path's flag - final path2 = _buildPathWithCurve( - points, - const LineChartCubicMonotoneCurve(), - ); - - final metrics1 = path1.computeMetrics().toList(); - final metrics2 = path2.computeMetrics().toList(); - - // Both paths should have identical length - expect( - metrics1.single.length, - closeTo(metrics2.single.length, 0.001), - ); + group('smooth parameter behavior', () { + test('the effect of smooth increases monotonically', () { + var smoothCount = 1; + var lastCurveLength = samplePoints1.straightDistance; + + while (smoothCount < 11) { + final smooth = 0.1 * smoothCount; + + final path = _buildPathWithCurve( + samplePoints1, + LineChartCubicMonotoneCurve( + smooth: smooth, + tinyThresholdSquared: 0, + ), + ); + final curveLength = path.computeMetrics().single.length; + expect(curveLength, greaterThanOrEqualTo(lastCurveLength)); + lastCurveLength = curveLength; + smoothCount++; + } }); }); }); } +void expectLikeStraightLine( + LineChartCurve curve, { + List points = samplePoints1, +}) { + final path = _buildPathWithCurve(points, curve); + + final metrics = path.computeMetrics().toList(); + expect(metrics.single.length, closeTo(points.straightDistance, 0.001)); +} + /// Helper function to build a complete path with the given curve Path _buildPathWithCurve(List points, LineChartCurve curve) { + final path = Path(); if (points.isEmpty) { - return Path(); + return path; } - final path = Path()..moveTo(points[0].dx, points[0].dy); + path.moveTo(points[0].dx, points[0].dy); for (var i = 1; i < points.length; i++) { final previous = points[i - 1]; @@ -213,3 +228,13 @@ Path _buildPathWithCurve(List points, LineChartCurve curve) { extension on Offset { double distanceTo(Offset other) => (this - other).distance; } + +extension on List { + double get straightDistance { + double distance = 0; + for (var i = 1; i < length; i++) { + distance += this[i].distanceTo(this[i - 1]); + } + return distance; + } +} From 52c9b8221b687782462303c2fa324738518e21a8 Mon Sep 17 00:00:00 2001 From: hhh <1224991097@qq.com> Date: Tue, 28 Oct 2025 14:31:36 +0800 Subject: [PATCH 08/10] update test for LineChartCubicMonotoneCurve.monotone --- .../line_chart/line_chart_curve_test.dart | 143 +++++++++++++++++- 1 file changed, 137 insertions(+), 6 deletions(-) diff --git a/test/chart/line_chart/line_chart_curve_test.dart b/test/chart/line_chart/line_chart_curve_test.dart index 3c58f7c4a..d3e18cca2 100644 --- a/test/chart/line_chart/line_chart_curve_test.dart +++ b/test/chart/line_chart/line_chart_curve_test.dart @@ -4,7 +4,7 @@ import 'package:fl_chart/fl_chart.dart'; import 'package:fl_chart/src/chart/line_chart/line_chart_curve.dart'; import 'package:flutter_test/flutter_test.dart'; -const samplePoints1 = [ +const testPoints1 = [ Offset(10, 10), Offset(20, 20), Offset(30, 40), @@ -157,7 +157,7 @@ void main() { test('draw curved line if distance > tinyThreshold', () { final path = _buildPathWithCurve( - samplePoints1, + testPoints1, const LineChartCubicMonotoneCurve(tinyThresholdSquared: 0), ); @@ -165,7 +165,7 @@ void main() { expect( metrics.single.length, - greaterThan(samplePoints1.straightDistance), + greaterThan(testPoints1.straightDistance), ); }); }); @@ -173,13 +173,13 @@ void main() { group('smooth parameter behavior', () { test('the effect of smooth increases monotonically', () { var smoothCount = 1; - var lastCurveLength = samplePoints1.straightDistance; + var lastCurveLength = testPoints1.straightDistance; while (smoothCount < 11) { final smooth = 0.1 * smoothCount; final path = _buildPathWithCurve( - samplePoints1, + testPoints1, LineChartCubicMonotoneCurve( smooth: smooth, tinyThresholdSquared: 0, @@ -192,12 +192,143 @@ void main() { } }); }); + + group('monotone constraint behavior', () { + test('SmoothMonotone.x prevents Y-direction overshoot with zigzag data', + () { + final points = [ + const Offset(0, 15), + const Offset(10, -50), + const Offset(20, -56.5), + const Offset(30, -46.5), + const Offset(40, -22.1), + const Offset(50, -2.5), + ]; + + const curve = LineChartCubicMonotoneCurve( + monotone: SmoothMonotone.x, + smooth: 0.3, + tinyThresholdSquared: 0, + ); + + final path = _buildPathWithCurve(points, curve); + final samples = _samplePath(path, 100); + + // Verify that Y coordinates of each curve segment stay within + // the Y range of its two endpoints + for (var i = 0; i < points.length - 1; i++) { + final minY = + points[i].dy < points[i + 1].dy ? points[i].dy : points[i + 1].dy; + final maxY = + points[i].dy > points[i + 1].dy ? points[i].dy : points[i + 1].dy; + + final segmentSamples = samples + .where((s) => s.dx >= points[i].dx && s.dx <= points[i + 1].dx); + + for (final sample in segmentSamples) { + expect( + sample.dy, + inRange(minY - 1.0, maxY + 1.0), + reason: 'Segment $i: Y=${sample.dy} out of range [$minY, $maxY]', + ); + } + } + }); + + test( + 'SmoothMonotone.y prevents X-direction overshoot with horizontal zigzag', + () { + final points = [ + const Offset(50, 0), + const Offset(10, 10), + const Offset(90, 20), + const Offset(20, 30), + const Offset(80, 40), + ]; + + const curve = LineChartCubicMonotoneCurve( + monotone: SmoothMonotone.y, + smooth: 0.3, + tinyThresholdSquared: 0, + ); + + final path = _buildPathWithCurve(points, curve); + final samples = _samplePath(path, 100); + + // Verify that X coordinates of each curve segment stay within + // the X range of its two endpoints + for (var i = 0; i < points.length - 1; i++) { + final minX = + points[i].dx < points[i + 1].dx ? points[i].dx : points[i + 1].dx; + final maxX = + points[i].dx > points[i + 1].dx ? points[i].dx : points[i + 1].dx; + + final segmentSamples = samples + .where((s) => s.dy >= points[i].dy && s.dy <= points[i + 1].dy); + + for (final sample in segmentSamples) { + expect( + sample.dx, + inRange(minX - 1.0, maxX + 1.0), + reason: 'Segment $i: X=${sample.dx} out of range [$minX, $maxX]', + ); + } + } + }); + }); }); } +/// Sample points uniformly along the path +List _samplePath(Path path, int sampleCount) { + final samples = []; + final metrics = path.computeMetrics().first; + + for (var i = 0; i <= sampleCount; i++) { + final distance = metrics.length * i / sampleCount; + final tangent = metrics.getTangentForOffset(distance); + if (tangent != null) { + samples.add(tangent.position); + } + } + + return samples; +} + +/// Custom range matcher +Matcher inRange(num min, num max) => _InRangeMatcher(min, max); + +class _InRangeMatcher extends Matcher { + const _InRangeMatcher(this.min, this.max); + + final num min; + final num max; + + @override + bool matches(dynamic item, Map matchState) { + if (item is! num) return false; + return item >= min && item <= max; + } + + @override + Description describe(Description description) { + return description.add('in range [$min, $max]'); + } + + @override + Description describeMismatch( + dynamic item, + Description mismatchDescription, + Map matchState, + bool verbose, + ) { + return mismatchDescription.add('was $item, outside range [$min, $max]'); + } +} + void expectLikeStraightLine( LineChartCurve curve, { - List points = samplePoints1, + List points = testPoints1, }) { final path = _buildPathWithCurve(points, curve); From 47e5dbc6aeb950dc0f7ba09daeb9158ade040975 Mon Sep 17 00:00:00 2001 From: hhh <1224991097@qq.com> Date: Tue, 28 Oct 2025 15:38:34 +0800 Subject: [PATCH 09/10] update documents --- CHANGELOG.md | 48 ++++++++++++++++++++++++- repo_files/documentations/line_chart.md | 20 ++++++++--- 2 files changed, 63 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e20f3c45f..08251745a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,51 @@ -## newVersion +## 1.2.0 * **BUGFIX** (by @imaNNeo) Consider the `enabled` property in [LineTouchData](https://github.com/imaNNeo/fl_chart/blob/main/repo_files/documentations/line_chart.md#linetouchdata-read-about-touch-handling), [BarTouchData](https://github.com/imaNNeo/fl_chart/blob/main/repo_files/documentations/bar_chart.md#bartouchdata-read-about-touch-handling), [PieTouchData](https://github.com/imaNNeo/fl_chart/blob/main/repo_files/documentations/pie_chart.md#pietouchdata-read-about-touch-handling), [ScatterTouchData](https://github.com/imaNNeo/fl_chart/blob/main/repo_files/documentations/scatter_chart.md#scattertouchdata-read-about-touch-handling), [RadarTouchData](https://github.com/imaNNeo/fl_chart/blob/main/repo_files/documentations/radar_chart.md#radartouchdata-read-about-touch-handling) and [CandlestickTouchData](https://github.com/imaNNeo/fl_chart/blob/main/repo_files/documentations/candlestick_chart.md#candlesticktouchdata-read-about-touch-handling), #1676 +* **BREAKING** ⚠️ (by @huanghui1998hhh) Enhanced line chart curve function with a new extensible curve system. Introduced `LineChartCurve` abstract class. You can implement your own curve or use built-in curve: `LineChartCubicTensionCurve`(old curve implementation), and `LineChartCubicMonotoneCurve`. +**Migration Guide:** + +Old API (deprecated): +```dart +LineChartBarData( + spots: spots, + isCurved: true, + curveSmoothness: 0.35, + preventCurveOverShooting: true, + preventCurveOvershootingThreshold: 10.0, +) +``` + +New API: +```dart +LineChartBarData( + spots: spots, + curve: LineChartCurve.cubicTension( // or use LineChartCubicTensionCurve() + smoothness: 0.35, + preventCurveOverShooting: true, + preventCurveOvershootingThreshold: 10.0, + ), +) +``` + +Or use the new monotone curve: +```dart +LineChartBarData( + spots: spots, + curve: LineChartCurve.cubicMonotone( // or use LineChartCubicMonotoneCurve() + smooth: 0.5, + monotone: SmoothMonotone.x, // Prevents overshooting along X-axis + ), +) +``` + +For straight lines (isCurved: false): +```dart +LineChartBarData( + spots: spots, + curve: LineChartCurve.noCurve, // or omit the curve parameter (default) +) +``` + +Check the updated [LineChart documentation](https://github.com/imaNNeo/fl_chart/blob/main/repo_files/documentations/line_chart.md#linechartcurve) for more details. ## 1.1.1 * **IMPROVEMENT** (by @imaNNeo) Upgrade `vector_math` dependency to `2.2.0`, #1985 diff --git a/repo_files/documentations/line_chart.md b/repo_files/documentations/line_chart.md index d8f30a79d..45990c647 100644 --- a/repo_files/documentations/line_chart.md +++ b/repo_files/documentations/line_chart.md @@ -47,10 +47,7 @@ When you change the chart's state, it animates to the new state internally (usin |gradient| You can use any [Gradient](https://api.flutter.dev/flutter/dart-ui/Gradient-class.html) here. such as [LinearGradient](https://api.flutter.dev/flutter/painting/LinearGradient-class.html) or [RadialGradient](https://api.flutter.dev/flutter/painting/RadialGradient-class.html)|null| |gradientArea| determines the area where the gradient is applied |null| |barWidth| gets the stroke width of the line bar|2.0| -|isCurved| curves the corners of the line on the spot's positions| false| -|curveSmoothness| smoothness radius of the curve corners (works when isCurved is true) | 0.35| -|preventCurveOverShooting|prevent overshooting when draw curve line on linear sequence spots, check this [issue](https://github.com/imaNNeo/fl_chart/issues/25)| false| -|preventCurveOvershootingThreshold|threshold for applying prevent overshooting algorithm | 10.0| +|curve| determines the curve style for drawing segments between spots (use [LineChartCurve](#LineChartCurve) types)| LineChartCurve.noCurve (straight lines)| |isStrokeCapRound| determines whether start and end of the bar line is Qubic or Round | false| |isStrokeJoinRound| determines whether stroke joins have a round shape or a sharp edge | false| |belowBarData| check the [BarAreaData](#BarAreaData) |BarAreaData| @@ -63,6 +60,21 @@ When you change the chart's state, it animates to the new state internally (usin |lineChartStepData|Holds data for representing a Step Line Chart, and works only if [isStepChart] is true.|[LineChartStepData](#LineChartStepData)()| |errorIndicatorData|Holds data for representing an error indicator (you see the error indicators if you provide the `xError` or `yError` in the [FlSpot](base_chart.md#FlSpot)).|[ErrorIndicatorData()](base_chart.md#FlErrorIndicatorData)| +### LineChartCurve +#### LineChartCubicTensionCurve +|PropName|Description|default value| +|:-------|:----------|:------------| +|smoothness| smoothness radius of the curve corners | 0.35| +|preventCurveOverShooting|prevent overshooting when draw curve line on linear sequence spots, check this [issue](https://github.com/imaNNeo/fl_chart/issues/25)| false| +|preventCurveOvershootingThreshold|threshold for applying prevent overshooting algorithm | 10.0| + +#### LineChartCubicMonotoneCurve +|PropName|Description|default value| +|:-------|:----------|:------------| +|smooth| determines smoothness of the curve (0.0 = straight, 1.0 = roundest) | 0.5| +|monotone| monotonicity constraint (none, x, or y) to prevent overshooting along the specified axis | SmoothMonotone.none| +|tinyThresholdSquared| squared distance threshold to draw straight line when points are too close (avoids jitter) | 0.5| + ### LineChartStepData |PropName|Description|default value| |:-------|:----------|:------------| From e39739700ecf62cc1c3d41ab5adfdd9ad5774054 Mon Sep 17 00:00:00 2001 From: hhh <1224991097@qq.com> Date: Thu, 26 Feb 2026 22:21:34 +0800 Subject: [PATCH 10/10] revert: remove CHANGELOG.md changes --- CHANGELOG.md | 50 +++----------------------------------------------- 1 file changed, 3 insertions(+), 47 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 08251745a..8846289c9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,51 +1,7 @@ -## 1.2.0 +## newVersion * **BUGFIX** (by @imaNNeo) Consider the `enabled` property in [LineTouchData](https://github.com/imaNNeo/fl_chart/blob/main/repo_files/documentations/line_chart.md#linetouchdata-read-about-touch-handling), [BarTouchData](https://github.com/imaNNeo/fl_chart/blob/main/repo_files/documentations/bar_chart.md#bartouchdata-read-about-touch-handling), [PieTouchData](https://github.com/imaNNeo/fl_chart/blob/main/repo_files/documentations/pie_chart.md#pietouchdata-read-about-touch-handling), [ScatterTouchData](https://github.com/imaNNeo/fl_chart/blob/main/repo_files/documentations/scatter_chart.md#scattertouchdata-read-about-touch-handling), [RadarTouchData](https://github.com/imaNNeo/fl_chart/blob/main/repo_files/documentations/radar_chart.md#radartouchdata-read-about-touch-handling) and [CandlestickTouchData](https://github.com/imaNNeo/fl_chart/blob/main/repo_files/documentations/candlestick_chart.md#candlesticktouchdata-read-about-touch-handling), #1676 -* **BREAKING** ⚠️ (by @huanghui1998hhh) Enhanced line chart curve function with a new extensible curve system. Introduced `LineChartCurve` abstract class. You can implement your own curve or use built-in curve: `LineChartCubicTensionCurve`(old curve implementation), and `LineChartCubicMonotoneCurve`. -**Migration Guide:** - -Old API (deprecated): -```dart -LineChartBarData( - spots: spots, - isCurved: true, - curveSmoothness: 0.35, - preventCurveOverShooting: true, - preventCurveOvershootingThreshold: 10.0, -) -``` - -New API: -```dart -LineChartBarData( - spots: spots, - curve: LineChartCurve.cubicTension( // or use LineChartCubicTensionCurve() - smoothness: 0.35, - preventCurveOverShooting: true, - preventCurveOvershootingThreshold: 10.0, - ), -) -``` - -Or use the new monotone curve: -```dart -LineChartBarData( - spots: spots, - curve: LineChartCurve.cubicMonotone( // or use LineChartCubicMonotoneCurve() - smooth: 0.5, - monotone: SmoothMonotone.x, // Prevents overshooting along X-axis - ), -) -``` - -For straight lines (isCurved: false): -```dart -LineChartBarData( - spots: spots, - curve: LineChartCurve.noCurve, // or omit the curve parameter (default) -) -``` - -Check the updated [LineChart documentation](https://github.com/imaNNeo/fl_chart/blob/main/repo_files/documentations/line_chart.md#linechartcurve) for more details. +* **BUGFIX** (by @artshooter) Fix wrong bar chart color with small value, #1757 +* **FEATURE** (by @3ph) Add `horizontalMirrored` and `verticalMirrored` properties in our `LabelDirection` enum which is used in ([HorizontalLineLabel](https://github.com/imaNNeo/fl_chart/blob/main/repo_files/documentations/base_chart.md#HorizontalLineLabel) and [VerticalLineLabel](https://github.com/imaNNeo/fl_chart/blob/main/repo_files/documentations/base_chart.md#VerticalLineLabel)), #1890 ## 1.1.1 * **IMPROVEMENT** (by @imaNNeo) Upgrade `vector_math` dependency to `2.2.0`, #1985