The graph of an arbitrary function cannot be rendered accurately using Bézier curves. For MetaPost, there is a bpolynomial package for accurately rendering polynomials of up to order 3. But since the polynomial considered here is of order 4, that package is not of much help. When drawing graphs of higher-order polynomials or non-polynomial functions, some kind of piecewise interpolation is required.
The naive approach is to construct a path by connecting some sample points using MetaPost's ..
operator. This can lead to sufficient results. But even when interpolating well-behaving functions, the shape of such a path often looks strange due to arbitrary slopes and curvatures at the end-points.
Daniel Luecking's splines package provides a few macros for piecewise spline interpolation. When called with a false first parameter, the fcnspline
macro computes a Bézier path such that there is no curvature at the end-points (second derivative equates to zero). This leads to a bit more ‘regular’ shapes. The package has some more features, e.g., sample points don't need to be distributed evenly or you can add your own equations to influence path shape. But the manual is really terse on all that ...
The following example shows two paths, calculated using the naive approach (violet) and calculated via fcnspline(false)
(black). Note, how parentheses have been used in macro f
, since *
and **
operators have the same precedence in MetaPost. Drawing the hatched bars is left as an exercise. (There should be more than one hatching related MetaPost package.)
outputtemplate := "%j-%c.mps";
prologues := 2;
input splines
% Evaluate a function.
def f(expr x) =
3(x**4) - 11(x**3) + 12(x**2) - 4x + 1
enddef;
% Construct naive Bézier path.
% Parameters are left and right interval boundaries and the number
% of sample points.
def naive_path(expr l,r,k)=
(l, f(l))
for i = 1 upto k-1:
.. ((i/(k-1))[l,r], f((i/(k-1))[l,r]))
endfor
enddef;
% Construct list of sample points for use with splines package.
% Parameters are left and right interval boundaries and the number
% of sample points.
def list(expr l,r,k)=
(l, f(l))
for i = 1 upto k-1:
, ((i/(k-1))[l,r], f((i/(k-1))[l,r]))
endfor
enddef;
% Show sample points. Argument is a list of points.
def dodots (text t) =
for _loc = t:
drawdot (u*_loc) withcolor red;
endfor
enddef;
% Output scale.
numeric u; u:=1cm;
% Adjust arrow length.
ahlength := 2bp;
path xaxis, yaxis;
xaxis := (-.1u,0)--(1.1u,0);
yaxis := (0,-.1u)--(0,1.1u);
% interval and sample points
numeric a,b,k;
a := 0.1;
b := 1;
k := 9;
beginfig(1);
% Draw coordinate system.
drawarrow xaxis;
drawarrow yaxis;
% Draw graph by naive spline construction.
draw (naive_path(a,b,k)) scaled u withcolor .75(red+blue);
% Draw graph using splines package.
draw fcnspline(false)(list(a,b,k)) scaled u;
% Draw sample points.
dodots(list(a,b,k));
endfig;
end
Edit - To elaborate a bit on the example
For the function considered here, the angles of gradient at x = 0.1
and x = 1
are ca. -62 and -45 degrees, resp. With what I called the naive approach above we get a path with angles of -85 and -52 degrees, with macro fcnspline(false)
we get angles of -53 and -39 degrees. While both solutions are quite poor at resembling the behaviour at the end points, the splines package's solution is clearly better at x = 0.1
.
That doesn't mean the fcnspline
macro always generates a path with superior shape. But the zero curvature condition tends to avoid surprising shapes. Other advantages of the splines package are, it provides ready-made solutions for spline interpolation, and with the fcnspline
macro there's no need to refer to the derivative of a function. Only the actual function needs to be evaluated - with acceptable results.
One trick to improve path shape (there are many) - again without referring to the derivative - is to construct a path that covers a larger interval than needed, then cutting it to the considered interval via the subpath of
operator. That doesn't work for all functions, e.g., near pole points, but you get the idea.
In the example below, I've increased the interval by Δx on both sides, adding two sample points, and finally cut off the first and last path segments via subpath (1,k) of <path>
. For both approaches, paths have been drawn over those from the last example in a lighter shade. Angles of gradient for all four paths are written to the .log file.
outputtemplate := "%j-%c.mps";
prologues := 2;
input splines
% Evaluate a function.
def f(expr x) =
3(x**4) - 11(x**3) + 12(x**2) - 4x + 1
enddef;
% Construct naive Bézier path.
% Parameters are left and right interval boundaries and the number
% of sample points.
def naive_path(expr l,r,k)=
(l, f(l))
for i = 1 upto k-1:
.. ((i/(k-1))[l,r], f((i/(k-1))[l,r]))
endfor
enddef;
% Construct list of sample points for use with splines package.
% Parameters are left and right interval boundaries and the number
% of sample points.
def list(expr l,r,k)=
(l, f(l))
for i = 1 upto k-1:
, ((i/(k-1))[l,r], f((i/(k-1))[l,r]))
endfor
enddef;
% Show sample points. Argument is a list of points.
def dodots (text t) =
for _loc = t:
drawdot (u*_loc) withcolor .75(red+green);
endfor
enddef;
% Return a string containing angles of gradient at end points
% for a given path.
def anglesofgradient(expr p) =
decimal angle((postcontrol 0 of p) - (point 0 of p))
& " and "
& decimal angle((point length p of p) - (precontrol length p of p))
enddef;
% Output scale.
numeric u; u := 1cm;
% Adjust arrow length.
ahlength := 2bp;
% interval and sample points
numeric a,b,k,delta;
a := 0.1;
b := 1;
k := 9;
delta := (b-a)/(k-1);
beginfig(1);
path pn, pntwo, ps, pstwo;
% Graph using naive spline construction.
pn := naive_path(a, b, k);
pntwo := subpath (1,k) of (naive_path(a-delta, b+delta, k+2));
% Graph using splines package.
ps := fcnspline(false)(list(a, b, k));
pstwo := subpath (1,k) of fcnspline(false)(list(a-delta, b+delta, k+2));
% Draw graphs.
draw pn scaled u withcolor .75(red+blue) withpen pencircle scaled 1bp;
draw ps scaled u withcolor black withpen pencircle scaled .75bp;
draw pntwo scaled u withcolor .5[.75(red+blue),white] withpen pencircle scaled .4bp;
draw pstwo scaled u withcolor .5[black,white] withpen pencircle scaled .2bp;
% Draw sample points.
dodots(list(a,b,k));
% Show angles.
show "angles at end points";
show "pn (dark violet) : " & anglesofgradient(pn);
show "ps (black) : " & anglesofgradient(ps);
show "pntwo (light violet): " & anglesofgradient(pntwo);
show "pstwo (gray) : " & anglesofgradient(pstwo);
endfig;
end
These are the angles of gradient at the end points:
>> "pn (dark violet) : -84.5836 and -52.30644"
>> "ps (black) : -53.27785 and -38.72513"
>> "pntwo (light violet): -64.68489 and -44.6875"
>> "pstwo (gray) : -64.88264 and -46.19849"
gjktc.mp
. If I delete that input command the result looks nothing like your figure. If I uncomment your commented code, mpost complains atpenc 0.2pt
. Try to write a MWE that does not requiregjktc.mp
.