Once again, I gives me great pleasure to announce a new package has joined CRAN.
transformr is the spatial brother of tweenr and as with the tweenr update
a few months ago, this package is very much driven by the infrastructural needs
of gganimate. It is probably the last piece needed before I can begin
preparing gganimate for CRAN, so if you are waiting for that there is indeed
reason for celebration.
Becoming Spatial
As written above, transformr is tweenr for spatial data (spatial being used
in a very broad sense as any data that is partly coordinates). To understand
what this means we’ll briefly have to touch on a core concept of tweenr. What
is never said out loud, but generally implied, is that tweenr treats all
columns of the data frame as independent. This is generally a sound principle as
you don’t want values from other columns to influence how e.g. the colour
transitions between black and blue. As far as spatial is concerned, this
approach also works fine as each row in the data frame encodes a single
independent point in space or if there’s a one-to-one mapping between points
in a polygon. Alas, the devil’s in the detail, and tweenr breaks down in
magnificent ways if you try to tween between more complicated and heterogeneous
shapes, e.g. a star and a circle. This is not something unique to tweenr, mind
you, d3.js also has this limitation. The problems in d3 led
Noah Veltman to develop the
flubber javascript library. His reasons
for developing it is succintly described in the animation below, grabbed from
the readme of flubber
The Trials of the Polygon
So, what’s the deal with polygons exactly. Why don’t they just do as you expect
them to and morph naturally from one to the other. That sad state of affair is
that there are multiple reasons for that:
There might be discrepancy between the number of points that make up the two
polygons. This may lead to part of the shape simply appearing or disappearing at
the start or end of the tween.
The winding of the polygons may have a different angular offset and/or
direction. This means that the tween will include rotatation and/or inversion,
something that is often undesirable.
There may be a discrepancy in the number of polygons that make up the two
shapes you tween between and/or a discrepancy between the number of holes. As
with 1. this may lead to parts of the shapes suddenly appearing or disappearing
during the tween.
Running the Gauntlet
transformr tries to solve the three problems above in much the same way as
flubber does, at least conceptually. There are enough differences between how
Javascript and R (as well as d3 and tweenr) works with data, that I decided to
only take the ideas behind flubber and implement them in my own way, in a manner
fitting for R, rather than doing a direct port of the library. This means that
you cannot expect the two libraries to behave equivalently. Below is, at a very
high level, what transformr does to address the 3 problems outlined above:
Points are added along the edges of the shape with the fewest corners until
the number of points matches between the shapes. Points are added so that long
edges will be divided more often than short edges in order to even out the edge
lengths of the final shape. Further, if any shape has fewer than a given number
of corners, points will be added (following the same strategy) until the number
of corners is reached.
After the number of points are evened out, the winding direction is matched
between the shapes (as clockwise), and the last shape is rotated until the
squared distance between point pairs of the two shapes is minimised.
This is adressed first (but is the least prevalent problem so it is mentioned
last). If there are different number of polygons in the two states you wish to
tween between, the polygons in the state with the fewest polygons is cut until
the number matches. Once again, the cuts are distributed so that large polygons
are cut more often than small. After the cutting, polygons between the states
are matched by minimising distance and area difference. If there are differences
in the number of holes in the matched polygons zero-area holes are inserted at
the gravitational center of the polygon with the fewest holes until the number
matches.
The Ways of the Transformr
At this point we have only talked about shapes (and polygons), so let’s get a
bit more concrete. transformr currently recognises three data types:
polygons, paths, and simple features. Polygons encompass simple polygons
as well as polygons with any number of holes. Paths can be either single or
multipaths. Simple features as implemented by the sf package are supported,
currently covering the (multi)point, (multi)path, and (multi)polygon types.
In terms of tween type support, transformr currently extends the
tween_state() API from tweenr but support for the other types of tweeners
will be added with time.
Some Examples
At this point an example is probably in order. We’ll start with what we first
identified as a problematic case: morphing between a circle and a star:
library(transformr)
library(ggplot2)
# Helpers included in transformer
circle