1212 * arithmetic only, unit-tested in isolation.
1313 *
1414 * <p>Values map linearly: the run's minimum sits on {@code y = 0}, its maximum
15- * on {@code y = 1}; a flat run centres on {@code y = 0.5}. Points are evenly
16- * spaced across {@code x = 0..1}.</p>
15+ * on {@code y = 1}; a flat run centres on {@code y = 0.5}. Data points are
16+ * evenly spaced across {@code x = 0..1}, and the polyline between them is
17+ * smoothed with the same uniform Catmull-Rom curve the chart engine uses,
18+ * densified to {@value #SMOOTH_SUBDIVISIONS} sub-segments per span — at
19+ * sparkline sizes the facets are far below visual resolution, so the run
20+ * reads as a true curve while staying a deterministic polygon ring.</p>
1721 *
1822 * @author Artem Demchyshyn
1923 * @since 1.8.0
2024 */
2125final class SparklineGeometry {
2226
27+ /**
28+ * Sub-segments per data span. Inline shapes stay polygon rings, so the
29+ * curve is densified instead of emitted as Béziers; 12 segments on a
30+ * ~40 pt sparkline puts every facet under half a point.
31+ */
32+ private static final int SMOOTH_SUBDIVISIONS = 12 ;
33+
2334 private SparklineGeometry () {
2435 }
2536
2637 /**
27- * Area silhouette: the value polyline closed down to the baseline.
38+ * Area silhouette: the smoothed value curve closed down to the baseline.
2839 *
2940 * @param values at least two finite values
30- * @return closed ring of {@code n + 2} normalized vertices
41+ * @return closed ring of smoothed normalized vertices
3142 */
3243 static List <ShapePoint > areaPoints (double [] values ) {
33- double [] ys = normalize (values );
34- List <ShapePoint > points = new ArrayList <>(ys .length + 2 );
35- for (int i = 0 ; i < ys . length ; i ++ ) {
36- points .add (new ShapePoint (x ( i , ys . length ), ys [ i ]));
44+ double [][] curve = smoothCurve ( normalize (values ) );
45+ List <ShapePoint > points = new ArrayList <>(curve .length + 2 );
46+ for (double [] p : curve ) {
47+ points .add (new ShapePoint (p [ 0 ], p [ 1 ]));
3748 }
3849 points .add (new ShapePoint (1.0 , 0.0 ));
3950 points .add (new ShapePoint (0.0 , 0.0 ));
@@ -48,7 +59,8 @@ static List<ShapePoint> areaPoints(double[] values) {
4859 *
4960 * @param values at least two finite values
5061 * @param thicknessFraction band thickness as a fraction of the box height, in (0, 1)
51- * @return closed ring of {@code 2n} normalized vertices
62+ * @return closed ring of smoothed normalized vertices (top edge forward,
63+ * bottom edge back)
5264 */
5365 static List <ShapePoint > ribbonPoints (double [] values , double thicknessFraction ) {
5466 if (thicknessFraction <= 0 || thicknessFraction >= 1 || Double .isNaN (thicknessFraction )) {
@@ -62,16 +74,58 @@ static List<ShapePoint> ribbonPoints(double[] values, double thicknessFraction)
6274 for (int i = 0 ; i < ys .length ; i ++) {
6375 ys [i ] = half + ys [i ] * (1.0 - thicknessFraction );
6476 }
65- List <ShapePoint > points = new ArrayList <>(ys .length * 2 );
66- for (int i = 0 ; i < ys .length ; i ++) {
67- points .add (new ShapePoint (x (i , ys .length ), ys [i ] + half ));
77+ double [][] curve = smoothCurve (ys );
78+ // Clamp the band CENTRE into [half, 1 - half] (spline overshoot may
79+ // poke past the compressed range) so the ±half offsets stay inside
80+ // the unit box without eating into the band thickness.
81+ List <ShapePoint > points = new ArrayList <>(curve .length * 2 );
82+ for (double [] p : curve ) {
83+ double centre = Math .max (half , Math .min (1.0 - half , p [1 ]));
84+ points .add (new ShapePoint (p [0 ], centre + half ));
6885 }
69- for (int i = ys .length - 1 ; i >= 0 ; i --) {
70- points .add (new ShapePoint (x (i , ys .length ), ys [i ] - half ));
86+ for (int i = curve .length - 1 ; i >= 0 ; i --) {
87+ double centre = Math .max (half , Math .min (1.0 - half , curve [i ][1 ]));
88+ points .add (new ShapePoint (curve [i ][0 ], centre - half ));
7189 }
7290 return points ;
7391 }
7492
93+ /**
94+ * Densifies the evenly-spaced value run with a uniform Catmull-Rom curve
95+ * (tension 0.5, clamped endpoints) — the same spline the chart engine
96+ * draws as native Béziers. Returns {@code (x, y)} samples including every
97+ * original point; y is clamped to the unit box because the spline may
98+ * overshoot slightly around extremes.
99+ */
100+ private static double [][] smoothCurve (double [] ys ) {
101+ int spans = ys .length - 1 ;
102+ double [][] out = new double [spans * SMOOTH_SUBDIVISIONS + 1 ][2 ];
103+ out [0 ] = new double []{0.0 , clamp01 (ys [0 ])};
104+ int n = 1 ;
105+ for (int i = 0 ; i < spans ; i ++) {
106+ double p0 = ys [Math .max (0 , i - 1 )];
107+ double p1 = ys [i ];
108+ double p2 = ys [i + 1 ];
109+ double p3 = ys [Math .min (ys .length - 1 , i + 2 )];
110+ for (int s = 1 ; s <= SMOOTH_SUBDIVISIONS ; s ++) {
111+ double t = (double ) s / SMOOTH_SUBDIVISIONS ;
112+ double t2 = t * t ;
113+ double t3 = t2 * t ;
114+ double y = 0.5 * ((2 * p1 )
115+ + (-p0 + p2 ) * t
116+ + (2 * p0 - 5 * p1 + 4 * p2 - p3 ) * t2
117+ + (-p0 + 3 * p1 - 3 * p2 + p3 ) * t3 );
118+ double x = (i + t ) / spans ;
119+ out [n ++] = new double []{x , clamp01 (y )};
120+ }
121+ }
122+ return out ;
123+ }
124+
125+ private static double clamp01 (double v ) {
126+ return Math .max (0.0 , Math .min (1.0 , v ));
127+ }
128+
75129 private static double [] normalize (double [] values ) {
76130 if (values == null || values .length < 2 ) {
77131 throw new IllegalArgumentException ("sparkline needs at least two values" );
@@ -95,8 +149,4 @@ private static double[] normalize(double[] values) {
95149 }
96150 return ys ;
97151 }
98-
99- private static double x (int index , int count ) {
100- return (double ) index / (count - 1 );
101- }
102152}
0 commit comments