@@ -4,7 +4,7 @@ import 'package:fl_chart/fl_chart.dart';
44import  'package:fl_chart/src/chart/line_chart/line_chart_curve.dart' ;
55import  'package:flutter_test/flutter_test.dart' ;
66
7- const  samplePoints1  =  [
7+ const  testPoints1  =  [
88  Offset (10 , 10 ),
99  Offset (20 , 20 ),
1010  Offset (30 , 40 ),
@@ -157,29 +157,29 @@ void main() {
157157
158158      test ('draw curved line if distance > tinyThreshold' , () {
159159        final  path =  _buildPathWithCurve (
160-           samplePoints1 ,
160+           testPoints1 ,
161161          const  LineChartCubicMonotoneCurve (tinyThresholdSquared:  0 ),
162162        );
163163
164164        final  metrics =  path.computeMetrics ().toList ();
165165
166166        expect (
167167          metrics.single.length,
168-           greaterThan (samplePoints1 .straightDistance),
168+           greaterThan (testPoints1 .straightDistance),
169169        );
170170      });
171171    });
172172
173173    group ('smooth parameter behavior' , () {
174174      test ('the effect of smooth increases monotonically' , () {
175175        var  smoothCount =  1 ;
176-         var  lastCurveLength =  samplePoints1 .straightDistance;
176+         var  lastCurveLength =  testPoints1 .straightDistance;
177177
178178        while  (smoothCount <  11 ) {
179179          final  smooth =  0.1  *  smoothCount;
180180
181181          final  path =  _buildPathWithCurve (
182-             samplePoints1 ,
182+             testPoints1 ,
183183            LineChartCubicMonotoneCurve (
184184              smooth:  smooth,
185185              tinyThresholdSquared:  0 ,
@@ -192,12 +192,143 @@ void main() {
192192        }
193193      });
194194    });
195+ 
196+     group ('monotone constraint behavior' , () {
197+       test ('SmoothMonotone.x prevents Y-direction overshoot with zigzag data' ,
198+           () {
199+         final  points =  [
200+           const  Offset (0 , 15 ),
201+           const  Offset (10 , - 50 ),
202+           const  Offset (20 , - 56.5 ),
203+           const  Offset (30 , - 46.5 ),
204+           const  Offset (40 , - 22.1 ),
205+           const  Offset (50 , - 2.5 ),
206+         ];
207+ 
208+         const  curve =  LineChartCubicMonotoneCurve (
209+           monotone:  SmoothMonotone .x,
210+           smooth:  0.3 ,
211+           tinyThresholdSquared:  0 ,
212+         );
213+ 
214+         final  path =  _buildPathWithCurve (points, curve);
215+         final  samples =  _samplePath (path, 100 );
216+ 
217+         // Verify that Y coordinates of each curve segment stay within 
218+         // the Y range of its two endpoints 
219+         for  (var  i =  0 ; i <  points.length -  1 ; i++ ) {
220+           final  minY = 
221+               points[i].dy <  points[i +  1 ].dy ?  points[i].dy :  points[i +  1 ].dy;
222+           final  maxY = 
223+               points[i].dy >  points[i +  1 ].dy ?  points[i].dy :  points[i +  1 ].dy;
224+ 
225+           final  segmentSamples =  samples
226+               .where ((s) =>  s.dx >=  points[i].dx &&  s.dx <=  points[i +  1 ].dx);
227+ 
228+           for  (final  sample in  segmentSamples) {
229+             expect (
230+               sample.dy,
231+               inRange (minY -  1.0 , maxY +  1.0 ),
232+               reason:  'Segment $i : Y=${sample .dy } out of range [$minY , $maxY ]' ,
233+             );
234+           }
235+         }
236+       });
237+ 
238+       test (
239+           'SmoothMonotone.y prevents X-direction overshoot with horizontal zigzag' ,
240+           () {
241+         final  points =  [
242+           const  Offset (50 , 0 ),
243+           const  Offset (10 , 10 ),
244+           const  Offset (90 , 20 ),
245+           const  Offset (20 , 30 ),
246+           const  Offset (80 , 40 ),
247+         ];
248+ 
249+         const  curve =  LineChartCubicMonotoneCurve (
250+           monotone:  SmoothMonotone .y,
251+           smooth:  0.3 ,
252+           tinyThresholdSquared:  0 ,
253+         );
254+ 
255+         final  path =  _buildPathWithCurve (points, curve);
256+         final  samples =  _samplePath (path, 100 );
257+ 
258+         // Verify that X coordinates of each curve segment stay within 
259+         // the X range of its two endpoints 
260+         for  (var  i =  0 ; i <  points.length -  1 ; i++ ) {
261+           final  minX = 
262+               points[i].dx <  points[i +  1 ].dx ?  points[i].dx :  points[i +  1 ].dx;
263+           final  maxX = 
264+               points[i].dx >  points[i +  1 ].dx ?  points[i].dx :  points[i +  1 ].dx;
265+ 
266+           final  segmentSamples =  samples
267+               .where ((s) =>  s.dy >=  points[i].dy &&  s.dy <=  points[i +  1 ].dy);
268+ 
269+           for  (final  sample in  segmentSamples) {
270+             expect (
271+               sample.dx,
272+               inRange (minX -  1.0 , maxX +  1.0 ),
273+               reason:  'Segment $i : X=${sample .dx } out of range [$minX , $maxX ]' ,
274+             );
275+           }
276+         }
277+       });
278+     });
195279  });
196280}
197281
282+ /// Sample points uniformly along the path 
283+ List <Offset > _samplePath (Path  path, int  sampleCount) {
284+   final  samples =  < Offset > [];
285+   final  metrics =  path.computeMetrics ().first;
286+ 
287+   for  (var  i =  0 ; i <=  sampleCount; i++ ) {
288+     final  distance =  metrics.length *  i /  sampleCount;
289+     final  tangent =  metrics.getTangentForOffset (distance);
290+     if  (tangent !=  null ) {
291+       samples.add (tangent.position);
292+     }
293+   }
294+ 
295+   return  samples;
296+ }
297+ 
298+ /// Custom range matcher 
299+ Matcher  inRange (num  min, num  max) =>  _InRangeMatcher (min, max);
300+ 
301+ class  _InRangeMatcher  extends  Matcher  {
302+   const  _InRangeMatcher (this .min, this .max);
303+ 
304+   final  num  min;
305+   final  num  max;
306+ 
307+   @override 
308+   bool  matches (dynamic  item, Map <dynamic , dynamic > matchState) {
309+     if  (item is !  num ) return  false ;
310+     return  item >=  min &&  item <=  max;
311+   }
312+ 
313+   @override 
314+   Description  describe (Description  description) {
315+     return  description.add ('in range [$min , $max ]' );
316+   }
317+ 
318+   @override 
319+   Description  describeMismatch (
320+     dynamic  item,
321+     Description  mismatchDescription,
322+     Map <dynamic , dynamic > matchState,
323+     bool  verbose,
324+   ) {
325+     return  mismatchDescription.add ('was $item , outside range [$min , $max ]' );
326+   }
327+ }
328+ 
198329void  expectLikeStraightLine (
199330  LineChartCurve  curve, {
200-   List <Offset > points =  samplePoints1 ,
331+   List <Offset > points =  testPoints1 ,
201332}) {
202333  final  path =  _buildPathWithCurve (points, curve);
203334
0 commit comments