by Paul Murrell http://orcid.org/0000-0002-3224-8858
This document is licensed under a Creative Commons Attribution 4.0 International License.
This document describes the 'vwline' package, which provides an R interface for drawing variable-width curves. The package provides functions to draw line segments through a set of locations, or a smooth curve relative to a set of control points, with the width of the line allowed to vary along the length of the line.
This document describes the 'vwline' package for drawing variable-width curves in R. For example, of the three lines shown below, standard R graphics can draw lines A and B (a line between two points, with a constant width) but not line C (a line between two points with a variable width).
It is possible in R graphics to draw line C as
a filled polygon, by explicitly specifying the outline of the
line boundary. For example, in this case the boundary is just a triangle.
Several R packages provide functions for drawing polygons of this
sort, where the boundary is completely specified from data, or
the boundary can be easily calculated from data as a simple
x-shift or y-shift. Examples include
geom_smooth) in 'ggplot2',
kiteChart in 'plotrix', and
the variations on "violin plots"
in the 'beanplot' package.
However, in situations where the line itself is not simple and the width is not just an x-shift or y-shift calculation, the description of the boundary can be much less straightforward. In the example below, the line is an X-spline, which R can draw as a line with constant width (lines D and E), but when the width of the line is allowed to vary, standard R graphics cannot help, and it is not straightforward to represent the line as a polygon because the boundary becomes a non-trivial path (line F).
The 'vwline' package provides a set of functions that make it easy to specify a variable-width line, like line F above.
The ability to describe variable-width lines has some direct applications to producing plots. For example, there are variable-width lines in Minard's famous map and VanderPlas and Hofmann have discussed the importance of controlling line widths with regard to the sine illusion. However, the work described in this document is more focused on adding a basic drawing primitive to R graphics. The general motivation is to provide these facilities in R so that users can avoid having to manually "touch up" plots in other software. For example, in the plot below (provided by Thomas Lumley, using data from the Auckland Transport real-time API), the plot was drawn with R, but the blue and orange arrows at the top-right of the plot were added using a drawing program outside of R.
It is better if all actions that make up a final plot can be captured in code and therefore recorded, automated, and shared. In this particular context, the variable-width line facility is meant to obviate the need for something like Adobe Illustrator's variable-width stroke tool, or SynFig's Advanced Outline Layer. This work is also influenced by the SVG proposal for variable-width lines (also see the discussion by Doug Schepers) and the Inkscape power stroke proposal.
The first approach to drawing a variable-width line with 'vwline'
is provided by the
grid.vwcurve function. This function
accepts a set of x/y locations and a set of widths, all of which
recycle as necessary. The idea with this function is that we know
the placement of the line and the
width of the line at all points on a curve.
The function generates (and draws) a polygon using the following algorithm:
This function can be used to produce the sort of result that 'beanplot' and other packages already provide, but with the additional flexibility that the line does not have to be straight and the widths do not have to be aligned with the x-axis or with the y-axis. In the images below, the main variable-width line is thick and black; a thin white line is drawn to represent the x/y locations of the centre of the line.
The second approach to drawing variable-width lines, which is provided
grid.vwXspline function, also accepts
x/y locations, plus a set of widths, but this time the
locations describe control points for a curve, rather than
explicit locations for the line to pass through.
More specifically, with this approach the x/y locations describe the control points for an X-spline, and the widths describe an amount to offset additional control points to the left and right of the main control points. The algorithm used to generate a polygon is as follows:
This approach is similar to work on simulating brush strokes by Pham and, to a lesser extent, Chua. Klassen also discusses this sort of approach for representing fonts.
This function can be useful if we want to produce a variable-width curve, but do not want to specify every x/y location and width for the curve. In other words, we want to draw a smooth curve with smoothly-varying width, without having to specify the curve or the width in minute detail.
The third approach to drawing variable-width lines is
provided by the
vwline function. This is similar
vwcurve function, because every location
and width for the line is specified explicitly, but it is
designed for a situation where the main line really is a series
of straight line segments (e.g., Minard's map).
The difference is that this function treats each location
as a corner (where
vwcurve pretends that the line is
a smooth curve with no sharp corners).
polysimplifyfrom the 'polyclip' package).
This function produces a non-smooth line, which may be useful if we wish to emphasise that the width is only known at specific locations along the line.
The fourth approach to drawing variable-width lines, which is
provided by the
brushXspline function, takes
a set of x/y control points to describe the line (an X-spline), a
"brush" (a geometric shape)
that is used to "sweep" the line, plus a set of widths
that are used to scale the brush as it moves along the line.
This approach is not entirely unlike Knuth's MetaFont, though with far less control over the path that is to be swept.
function can be useful if we want to produce a variable-width
curve and do not want to specify every x/y location or width
for the curve (so cannot use
but we want the perpendicular width of the curve
to be more accurately represented (so cannot use
The examples below construct a
vwXspline and a
brushXspline through the same set of control points.
brushXspline case, we can see the effect
of the width being perpendicular to the line at all points along the line.
The final approach, provided by the
generates a mathematical description of
"offset curves" and draws those offset curves.
The problem of determining the outline for a curve, when the width is fixed, is known as generating an "offset curve" or an "offset polygon". This arises in a variety of contexts, including stroking a path with a non-zero line width, and producing buffer regions for spatial objects. With offset curves, we start with a mathematical definition of the curve (e.g., a Bezier curve), and we derive a mathematical expression for curves that are offset by a fixed amount, then we render the offset curve.
Examples of mathematical solutions for offset curves are often focused on Bezier curves, e.g., Hoschek and Hain et al. A review was given by Elber et al.
A general approach for offset polygons was described by Chen and McMains. This algorithm has been implemented in the Clipper library, which has an R interface via the polyclip package. The CGAL library can also generate polygon offsets, particularly the 2D Straight Skeleton and Polygon Offsetting package.
This function implements the general formulation for a variable offset curve that is described in Chen and Lin. The difficult part of this approach is determining a function for the offset curve (which requires a function for the unit normal to the main curve and a function for the width of the curve). However, once these equations have been found, the algorithm is straightforward.
polysimplifyfrom the 'polyclip' package).
The result from this function should be similar to the
brushXspline (as shown below).
offsetXspline function provides a more robust (less
heuristic) approach, but
brushXspline offers more
flexibility via the possibility of different brushes
(see Specifying brushes).
Having introduced the basic idea behind the main functions in the 'vwline' package, we will now explore how these functions work in a little more detail.
For the functions
we specify widths explicitly,
either as a width for
every x/y location on the line or as
a width for every x/y control point.
widths are just a numeric vector or a 'grid' unit and
the width is the same either side of the main curve.
vwline function is slightly more flexible and
accepts a numeric vector or
'grid' unit for the width, but also accepts a list of separate
left and right widths, as generated by the
function. This allows different widths to the left and right
of the main curve.
vwline function also has a
argument, which means that widths are not linearly interpolated along
the main curve, but remain constant along each segment (and the final
width is silently ignored).
the line width
is not as tightly coupled with the x/y locations.
For these functions, the x/y locations describe a curve
and the width argument separately describes how the line width
should vary along the length of the curve. The simplest approach
is to specify a numeric vector of width values, in which case the
first value is used as the width at the start of the curve, the last
value is used as the width at the end of the curve, and the other
widths are spaced evenly along the curve. By default, the
widths are interpreted as numbers of millimetres.
the change in width along the line is controlled by an X-spline
with a shape parameter of -1. The
brushXspline example from the previous section is
repeated below (the left image).
Notice that there are four x/y locations for
control points, but only three width values. This means that
the width of the line starts at 0, ends at 0, and is 0.2 in the
middle of the curve, with width varying smoothly according to an X-spline
through (0, 0), (0.5, 0.2), (1, 0). The image below on the right
shows what this width X-spline looks like.
The spline that controls the width for
offsetXspline can also be
specified explicitly with the
This allows the set of widths to be defined along with a set of distances
along the line (the x-values for the width X-spline).
The distances can just
be numeric values, in which case they are interpreted as proportions
of the line length, or they can be 'grid' units (e.g., millimetres).
There is also a
shape argument to control the shape of the
width X-spline (though note that positive shape values
will mean that the maximum explicit width may not be achieved; see
the appendix on X-splines). Finally, there
rep argument to control whether the widths
repeat or stay fixed for distances along the curve that are outside
the explicit distances specified by the width spline.
In the example below, we specify that the line width should start with a width of zero at one quarter of the distance along the line, rise smoothly to 1cm half way along the line, then drop again to zero at the three-quarter point, and the pattern should repeat towards either end of the line.
Several variable-width-line functions provide an
argument to control the angle at which the width is calculated or
at which the brush is placed. By default, this argument has the
"perp", but a numeric value can be
specified instead. For example, the brush in
brushXspline can be fixed upright (a rotation of 0)
as it sweeps the curve
(see the left image below),
vwXspline function can calculate left and right
control points at a fixed angle (here 45 degrees)
from the main control points
(see the right image below). This can produce something like
a calligraphic effect.
angle argument is not implemented for
vwline (because the result will be the same as for
vwcurve) or for
offsetXspline (because the offset curve is based on
unit normals, which are, by definition perpendicular to the main curve).
Almost all functions allow control over the line end style,
This is similar to line end styling on normal (fixed-width) lines,
so it is possible to specify
"round" line ends (the default is
However, because the edges of a variable-width
line are almost never parallel to the
main curve, there is an additional
"mitre" line end style.
For the same reason, a
"round" line ending does not
usually produce a semi-circle and a
"square" line ending
has two corners, but they are not usually square.
As demonstrated below, if the width is diverging at the end of the
line, a mitre end will automatically convert to a square end.
This conversion will also occur when a mitre end would be too long;
mitrelimit argument allows control over when
When the width is decreasing rapidly, it is also possible for
a square end to be automatically converted to a mitre.
Line end styles are not implemented for
because the line end shape is affected by the brush shape instead.
vwline function (and only that function) also provides a
linejoin argument for controlling the join style
on corners. This takes the value
"round" (the default),
The main difference compared to normal (fixed-width) lines is
that a round corner is not usually just an arc of a circle
(because the line edge typically approaches the corner at a different angle
from either side of the corner, as in the examples below).
Line joins are not implemented for
vwcurve() because edges already meet at a single point for
every corner. They are not implemented for
for similar reasons.
functions will accept a shape of 0, which produces a main curve
with sharp corners. The current behaviour for
in this case is effectively to produce a bevel join. The
brushXspline will depend on the brush
used, but is likely to produce a non-smooth (jaggy) corner.
brushXspline function, the
variable-width line is calculated
as the region swept out by
a "brush" shape. By default, this brush is a (very thin) vertical line
(that is rotated perpendicular to the line), but we can specify
alternative brush shapes.
Another predefined brush shape is provided by the
function, but a
brush is just a list of x/y locations within the range -1 to 1, so
we can define any shape we want. The examples below demonstrate the
circleBrush, which is most noticeable at the
line ends, and a custom brush that only sweeps the left (top)
half of the line.
All variable-width line functions also provide an
argument that can be set to
FALSE to make the
line "closed" (the last point is connected back to the first point).
In the example below, we place 10 control points in a circle (anticlockwise) and make a closed line (so the resulting curve is very close to a circle). We then sweep the curve with a half brush that varies smoothly between 2mm and 8mm (so the inside of the curve is swept at a width of 1mm to 4mm). The right-hand image below shows the shape of the width spline used in this example.
The width specifications for different functions will produce
different results for closed curves.
we provide a width at each location, so the first
width also becomes the last width (it automatically cycles).
we provide widths from the start to the end of the line,
so if we want a smooth
transition where the head of the line meets the tail of the line,
we need to make sure that the width at the start matches the width
at the end.
brushXspline sweeps the brush
continuously along the line, but the
argument also allows the brush
to be placed at only discrete locations along the line.
spacing argument is recycled to cover the
In the example below, we draw a vertical brush very 1mm along the line (left image) and a circle brush every .05 of the way along the line (right image), with the brush size varying along the line in both cases.
A "line" drawn by the functions in 'vwline' is actually a filled
polygon outline. By default, this polygon is drawn with no
outline and a black fill, but it is possible to specify different
The two examples from the previous section demonstrated this idea.
In the left image,
gp=gpar(col="black") is used to stroke the
outline of the brush (the brush is very thin so it is very hard
to see in isolation when it is only filled). In the right image,
gp=gpar(fill=NA) is used to override the default;
this means that the brush is not filled, but its outline is stroked.
The functions in 'vwline' follow the pattern of normal 'grid'
functions; there is a
grid.* version and a
*Grob version. Both create a grob to represent a
variable-width line, but only the first one draws anything.
Because we have a grob representing the line, we can do things
like edit the line with
grid.edit and export the
line with 'gridSVG'. The
'vwline' package also adds the ability to query a
variable-width line for points on its boundary. This can be
useful for drawing one variable-width line relative to another.
The code below creates a "vwXsplineGrob", draws it, then calculates five evenly-spaced locations along both edges of the grob. The result is a list of locations for both the left and right edge (see the left image below; the locations along the left edge are shown as red dots), plus a set of tangents at each location (the tangents are used to show perpendicular blue lines).
Both left and right edges are traversed in the same order as the control points (in this case, from left to right across the image), but we can reverse that direction. The code below calculates three points 1cm apart from the start of the left edge (red dots in the right image below) and three points 1cm apart from the end of the left edge (green dots in the right image below).
edgePoints function works as described above
However, the calculation of edge points is a little different for
because in those cases the boundary of the overall line
is not split nicely into separate "left" and "right" edges.
In order to control where to start on the boundary, we
must specify an "origin" and the nearest point on the boundary
to that origin
becomes location 0. By default, the boundary is traversed
anticlockwise, but again we can reverse that if desired.
The code below calculates three points 1cm apart
starting from the location closest to the centre left side of the image
and travelling clockwise around the boundary
(red dots in the right image below), and
three points 1cm apart starting from the location closest to the
centre right side of the image and travelling anticlockwise around
(green dots in the right image below).
Some complications arise when a variable-width line intersects
with itself. The variable-width line is drawn as a filled shape,
and if the shape intersects with itself there are different ways
to decide which parts of the shape to fill.
An example of this situation for
vwXspline is shown below.
The main line crosses itself, which means that the shape that
we generate to fill is self-intersecting (see the left image).
By default, the shape is filled as a path using the "non-zero
winding" rule (see the middle image). The
argument can be used to change to, for example, filling the path
using the "even-odd" rule (see the right image).
The situation is different for
the shape that we calculate is the union of a whole lot of brush
shapes. In the example below, the shape that is produced is a path
with a hole in it (see the left image). Again, by default, the
path is filled using "non-zero winding" (middle image), but we can
change that if we wish, for example, we can treat the shape as
a polygon rather than a path (right image).
The above example also demonstrates that
when we draw a self-intersecting, variable-width,
brushed line, we get multiple boundary lines.
If we wish to calculate edge points on the boundary
of such a line, we can specify which of the boundary
lines we want to work with (using the
A subtler version of this self-intersection issue occurs when the boundary of a variable-width line intersects with itself, even though the main line does not. This occurs when the main line has tight corners and/or the width of the line is changing rapidly.
The result for
vwXspline, as shown in the example below,
is a loop in the boundary (left image). The default rendering handles this
problem (middle image), though interesting effects can be had
by altering the rendering (right image).
This issue does not arise for
the union of brushes cannot create holes in the boundary ...
... however, it is possible to create some unusual shapes if the line
width changes rapidly relative to the line length. In these cases,
offsetXspline function will behave more robustly.
All functions have a
debug argument that allows the
addition of graphics debugging information to be drawn on the
When setting this to TRUE, it is usually useful to also
set gp=gpar(col="black") (so that the line is not filled black,
which would obscure most of the debugging information).
This section demonstrates a range of interesting results that can be achieved with variable-width lines. Only an indication of the required code is given in each case; the full code can be found in the source XML files (see the Resources Section).
The first examples demonstrate the use of an abrupt change in width (to produce arrow heads).
The next examples emulate the examples described in the discussion by Doug Schepers.
The next example demonstrates the use of tangents at boundary
points to arrange a set of variable-width lines along the
boundary of one larger variable-width line. The
function demonstrates the usefulness of defining a base
line within a viewport, so that variations of the base line
can be drawn conveniently at a variety of locations, angles, and sizes.
The next example demonstrates adding a gradient fill to a variable-width line with the 'gridSVG' package.
The next example shows brush spacing being used to place arrows part way along a curve
The next example provides the code for the infinity symbol from the start of this document.
The final example shows how the variable-width lines might be
useful in a plot. This is of course based on the data for Minard's
map. This demonstrates the use of
plus, for the return journey,
unequal line widths to the left and right.
The 'vwline' package provides several different convenience functions for generating shapes that represent a curved line with a varying width. These functions make it easy to produce a variety of shapes that would otherwise be arduous to construct with standard R graphics. The hope is that this will help to reduce the temptation for users to manually touch up images outside of R. In other words, the hope is that users will find it easier to construct images purely using (R) code.
In addition to the convenience, there are, to the author's knowledge, two novel contributions within the package. One is a general algorithm for producing line endings and line joins for variable-width curves and the other is code for general offset curves for variable-width X-splines. Additional documents are planned to describe these contributions in greater detail.
The first major drawback to the functions in the 'vwline' package
The only concession to efficiency that has been made is to
set the byte-compile flag in the 'vwline' package. This is
particularly aimed at the code underlying the
package, which involves naive and unoptimised functions for calculating
offset X-spline curves.
It is also the case that the functions in 'vwline' are designed only to
produce a single line per function call, so drawing a large number
of variable-width lines will be even slower.
Another weakness of the 'vwline' package is that most functions rely on
a heuristic/numerical approach,
so may be vulnerable to edge cases and numerical instability. Only the
offsetXspline function is based on an analytical approach
that should be free of those problems.
Finally, the lines drawn by 'vwline' are not really lines, but polygons (or paths). With real lines, in addition to line width, it is possible to control several other features including line endings (butt, square, and round), line joins (round, bevel, and mitre), and line types (dashed, dotted, etc). Although support is provided for line endings and line joins for most functions in 'vwline', there is currently no support for line types.
The examples and discussion in this document relate to 'vwline' version 0.1.
This document was generated within a Docker container (see Resources section below).
This appendix provides a brief review of
X-splines. An understanding of how to control the shape of
an X-spline is useful for both defining the main curve
of a variable-width line (for
and for defining the width of the line (for
X-splines are a family of curves, where the final curve is based on a set of control points. The curve approximates or interpolates the control points based on a single "shape" parameter. In the three examples below, the control points are drawn as dark grey circles connected by dashed lines; the top X-spline has a shape parameter of 1 (approximation), the middle X-spline has shape 0 (straight line segments), and the bottom X-spline has shape -1 (interpolation). The shape parameter can vary anywhere between -1 and 1.
It is also possible to specify a different shape for each control point (though the end control points must be zero). The second control point below has a shape of 0 and the third control point has a shape of 1.
Approximating X-splines produce nicer smooth curves, but do not pass through control points (except at the ends; see the top line below). Interpolating X-splines go through the control points, but can produce "bumpy" curves (see the middle line). An approximating X-spline with three collinear control points will pass through the central of the three control points (see the bottom line).
Using a not-quite-zero shape can produce a curve that appears to have sharp corners. The upper line below has shape 0; the lower line has shape 0.1.
By default, the first and last control points are repeated, which ensures that the X-spline starts and ends at the first and last x/y locations. This can be turned off, in which case the line only starts and ends alongside the second and second-to-last control point (see the middle image below). Instead of repeating the first and last control points, we can add extra control points that extend in the direction of the first and last control segment, thus ensuring that the line ends at the "first" and "last" control point, but producing a slightly more rounded curve (see the right image below).
Here is an example that shows development of an X-spline that combines smooth approximation with hitting the maximum, with a nice end asymptote.
As a convenience, when specifying the main curve for
repEnds argument also accepts the special value
"extend", which means that additional control points
are automatically added (with shape 0) that extend the direction
of the first and last control segments.
This document is licensed under a Creative Commons Attribution 4.0 International License.