1

Let's assume a sequence of points. If I would like to plot a smooth curve through these points, I thought the plot option smooth would do the job.

From help smooth:

Syntax:

  smooth {unique | frequency | fnormal | cumulative | cnormal | bins
                 | kdensity {bandwidth}
                 | csplines | acsplines | mcsplines | bezier | sbezier
                 | unwrap}

So, Bézier curves will not go through the points, but some of the splines should. However, in gnuplot splines require monotonic x-values. If they are not monotonic, gnuplot will make them monotonic, with (in this case) undesired results.

How can I draw a smooth curve through the points?

Example:

### smooth curve through points?
reset session
set size ratio -1

$Data <<EOD
0 0
2 3
4 2
9 3
5 7
3 6
4 5
5 5
4 4
1 6
1 4
3 10
EOD

set key out
set ytics 1

plot $Data u 1:2 w lp pt 7            lc "red" dt 3 ti "data", \
        '' u 1:2 w l smooth bezier    lc "green"    ti "bézier", \
        '' u 1:2 w l smooth csplines  lc "orange"   ti "csplines", \
        '' u 1:2 w l smooth mcsplines lc "magenta"  ti "mcsplines", \
        '' u 1:2 w l smooth acsplines lc "yellow"   ti "acsplines"
### end of code

Result: (none of the smooth options will give the desired result)

enter image description here

theozh
  • 22,244
  • 5
  • 28
  • 72

2 Answers2

2

Edit: Here is a completely revised and shortened version:

  • it uses arrays instead of datablocks (like in @Eldrad's solution)
  • x,y coordinates are stored in real and imaginary part of complex variables, respectively. This makes the parametric calculation shorter compared to two separate x,y variables.
  • use parameter r to tune the shape of the curve.

To my opinion, the "nicest" Bézier curve through the given points is plotted with the parameter r=0.333. As mentioned by @binzo, since gnuplot5.5, you have the option smooth path which is drawn for comparison.

Script: (requires gnuplot>=5.2.0 because of arrays, and gnuplot>=5.5 because of smooth path)

(skip the second plot line if you have gnuplot<5.5)

### plot cubix Bézier curve through given points
reset session

$Data <<EOD
0 0
2 3
4 2
9 3
5 7
3 6
4 5
5 5
4 4
1 6
1 4
3 10
EOD

set size ratio -1
set angle degrees
set key noautotitles reverse Left
set samples 200

colX     = 1
colY     = 2
j        = {0,1}                                 # imaginary unit
a(dx,dy) = dx==0 && dy==0 ? NaN : atan2(dy,dx)   # angle of segment between two points
L(dx,dy) = sqrt(dx**2 + dy**2)                   # length of segment
r        = 0.333                                 # relative distance of ctrl points

stats $Data u 0 nooutput   # get number of points+1
N = STATS_records+1
array P0[N]
array PA[N]
array PB[N]
array P1[N]

x1=x2=y1=y2=ap1=NaN
stats $Data u (x0=x1, x1=x2, x2=column(colX), i=int($0)+1, \
               y0=y1, y1=y2, y2=column(colY), P0[i]=x0+j*y0, \
               dx1=x1-x0, dy1=y1-y0, d1=L(dx1,dy1), dx1n=dx1/d1, dy1n=dy1/d1, \
               dx2=x2-x1, dy2=y2-y1, d2=L(dx2,dy2), dx2n=dx2/d2, dy2n=dy2/d2, \
               a1=a(dx1,dy1), a2=a(dx2,dy2), a1=a1!=a1?a2:a1, \
               ap0=ap1, ap1=a(cos(a1)+cos(a2),sin(a1)+sin(a2)), \
               PA[i]=x0+d1*r*cos(ap0) + j*(y0+d1*r*sin(ap0)), \
               PB[i]=x1-d1*r*cos(ap1) + j*(y1-d1*r*sin(ap1)), P1[i]=x1+j*y1, 0) nooutput
# add last segment
P0[i+1] = x1+j*y1
PA[i+1] = x1+d1*r*cos(ap1)+j*(y1+d1*r*sin(ap1))
PB[i+1] = x2-d2*r*cos(a2) +j*(y2-d2*r*sin(a2))
P1[i+1] = x2+j*y2

# Cubic Bézier function with t[0:1] as parameter between two points
# p0: start point, pa: 1st ctrl point, pb: 2nd ctrl point, p1: endpoint
p(i,t) = t**3 * (  -P0[i] + 3*PA[i] - 3*PB[i] + P1[i]) + \
         t**2 * ( 3*P0[i] - 6*PA[i] + 3*PB[i]        ) + \
         t    * (-3*P0[i] + 3*PA[i]                  ) + P0[i]

plot $Data u 1:2 w lp pt 7 lc "red" dt 3 ti "data", \
        '' u 1:2 smooth path w l lc "black" ti "smooth path", \
     for [i=2:|P0|] [0:1] '+' u (real(p(i,$1))):(imag(p(i,$1))) w l lc "blue" \
         ti i==2?("\nCubic Bézier\nthrough points"):''
### end of script

Result:

enter image description here

And for fun, an animation, varying the parameter r:

enter image description here

theozh
  • 22,244
  • 5
  • 28
  • 72
2

smooth path, as already written by @binzo in the comments, might give you the desired result. I'd like to share an alternative approach that I had developed for personal needs and which, similar to your own answer, defines the smoothing function manually and iterates over all points.

Here I chose a spline that connects two consecutive points and starts and ends horizontally (i.e. its derivative at (x1,y1) and (x2,y2) is 0)

spline(x,x1,y1,x2,y2) = y1+x1**2*(y1-y2)*(3*x2-x1)/(x1-x2)**3 + 6*x1*x2*(y2-y1)/(x1-x2)**3*abs(x) + 3*(y1-y2)*(x1+x2)/(x1-x2)**3*abs(x)**2 + 2*(y2-y1)/(x1-x2)**3*abs(x)**3

The spline is going to be plotted iteratively between each consecutive pair of points. In order to do so it is convenient to store the data block as arrays for later indexing:

$Data <<EOD
0 0
2 3
4 2
9 3
5 7
3 6
4 5
5 5
4 4
1 6
1 4
3 10
EOD

stats $Data noout
array xvals[STATS_records]
array yvals[STATS_records]
do for [i=1:|xvals|] {
  stats $Data every ::i-1::i-1 u (xvals[i]=$1,yvals[i]=$2) noout
}

In the iterative plot each single spline is plotted only within its respective [x1:x2] range. For comparison, smooth path is also included.

plot $Data w lp pt 7 lc "red" dt 3,\
 for [i=1:|xvals|-1] [xvals[i]:xvals[i+1]] spline(x,xvals[i],yvals[i],xvals[i+1],yvals[i+1]) lc "blue" not,\
 keyentry w l lc "blue" t "user-defined splines",\
 $Data smooth path lc "black" t "smooth path"

enter image description here

Alternatively, one can circumvent the transformation into arrays and directly access the elements of the data block. E.g. $Data[2] gives the second line as a string, which can be split by word(). In order to get proper floating point operations (instead of integer) in the end, the numbers have to be wrapped by real(), which makes the plotting command a bit more voluminous:

plot $Data w lp pt 7 lc "red" dt 3,\
 for [i=1:|$Data|-1] [word($Data[i],1):word($Data[i+1],1)] spline(x, real(word($Data[i],1)), real(word($Data[i],2)), real(word($Data[i+1],1)), real(word($Data[i+1],2)) ) w l lc "blue" not,\
 keyentry w l lc "blue" t "user-defined splines",\
 $Data smooth path lc "black" t "smooth path"

Which of the two smoothing options is better depends on what you want to achieve in the end. Obviously, this spline attempt fails where two consecutive points have the same x value.

Eldrad
  • 733
  • 3
  • 15
  • Thank you for your suggestion. I haven't yet looked into splines. In my case I was looking for a smooth curve which is not "changing direction" at certain points, i.e. continuous first derivative. I'm pretty sure somehow you can make your approach also fulfill this requirement and also work for two consecutive points with the same x-value. – theozh Sep 05 '21 at 16:51
  • @theozh Strictly speaking, the first derivative is already continuous in my answer ;-) However, before I put too much effort in it, could you please specify which type of smoothing/pathing you want, or how the analytical expression of the final curve should be defined? Or maybe `smooth path` is already fulfilling your needs? – Eldrad Sep 06 '21 at 21:20
  • you're right! Sorry, I meant without changing the path direction in a sharp corner. How would you describe this mathematically? `smooth path` in gnuplot 5.5 will certainly be the easiest way and sufficient, although, I like the shape of the Bézier better than the `smooth path` and you have some parameters to shape the curve a little. Maybe you also have some additional parameters with `smooth path`... I haven't installed gnuplot 5.5 yet. – theozh Sep 07 '21 at 05:53
  • @theozh I don't know how to express the "direction" mathematically, because the final path is not a function (one x has several y values). My solution provides piecewise functions, where the second derivative at the corners is not continuous. For `path` the concept of derivatives is not applicable. Anyway, I'd still like to clarify: For each two consecutive points, you want to draw a Bézier (because Béziers only go through the first and last point), right? What is your concept for defining the intermediate control points? What are the constraints? – Eldrad Sep 07 '21 at 10:09
  • yes, "2nd derivative continuous", that's what I was looking for. For a smooth path the requirement would be that at every point you can create a tangent (which you cannot at a sharp corner). In my Bézier approach, 1. the angles at a path point are the average of the angles of the 2 adjacent segments. 2. the control points are calculated relative to two consecutive points in the path and the average angle from 1. Some more detailed explanations are here: https://stackoverflow.com/a/60389081 . There are no special constraints for the control points. r=0.3 gives reasonably looking smooth curves. – theozh Sep 08 '21 at 07:51
  • @theozh I've never worked with Beziers in-depth, hence I would need to dig into their definition and understand how the shape is correlated with the control points. Unfortunately, this is more than I am up to do right now. Anyway, if I was to code that I would try to derive the coordinates of the intermediate points, make small datablocks and then iteratively plot with `smooth bezier` – hopefully this would result in a more readable code… – Eldrad Sep 11 '21 at 16:05
  • indeed, this would be another option. Nevertheless, I would have to calculate the two control points with some "less readable" code... this is what I am doing already ;-) and then let gnuplot plot the `smooth bezier`. If I find time, I will add this variation. Thanks for the suggestion. – theozh Sep 11 '21 at 16:12
  • BTW, you don't need a `for`-loop and `every` and read the file N times to get your values into the arrays, you can simply do: `stats $Data u (i=int($0+1),xvals[i]=$1,yvals[i]=$2) nooutput` – theozh Dec 03 '22 at 23:51