This D3 plugin allows to animate back and forth between two d3-voronoi-map.
Considering the data coming from either the starting data set or the ending data set, each single datum has a corresponding cell in the starting Voronoï map and another in the ending Voronoï map. The objective of the plugin is to provide a way (i.e. an interpolator function) to smoothly interpolate between the starting cell and the ending cell of each data.
To do so, the algorithm does not interpolate polygons associated to each single datum in order to no have a mess of overlapping cells (cf. this easy-but-unsatisfying attempt). But it rather interpolates the characteristics of the sites producing each polygon and then compute a Voronoï map of these interpolated sites (thanks to d3-weighted-voronoi). It also takes care of cells found only in the starting Voronoï map (data only available in the starting data set) or found only in the ending Voronoï map (data only in the ending data set).
Because a picture is worth a thousand words:
In this animation:
- blue cells are cells available in both the starting and ending Voronoï maps, i.e. data both in the starting and ending sets; these cells smoothly evolve in order to reflect their starting and ending weights, which may be distinct
- red cells are cells available only in the starting Voronoï map, i.e. data only in the starting data set; these cells smoothly disappear
- green cells are cells available only in the ending Voronoï map, i.e. data only in the ending data set; these cells smoothly appear
- 'evolving overall size' demonstrates that the plugin can handle animation between disks of distinct sizes; when enabled, the ending overall disk is smaller than the starting disk
- 'evolving overall shape' demonstrates that the plugin can handle animation between distinct shapes; when enabled, the ending overall shape is a pentagone; here, the smooth interpolation between the two shapes (circle and pentagon) is handled with flubber
- 'show internals' shows how the cells' sites evolve (they either appear/disappear/evolve in location and weight)
Available only for d3-voronoi-map v2.
Animating a Voronoï map is already possible with the live arrangement feature of the d3-voronoi-map plugin. This feature is sufficient to handle updates of data (displayed as evolving cell areas) for a static overall shape, but can't handle addition or deletion of data (deletion = data no longer existing at the end of the animation, addition = data not existing at the begining of the animation) and can't handle an evolving overall shape (e.g. a shape becoming bigger, representing the increase of the total amount).
This is where the d3-voronoi-map-tween comes in:
- added data are displayed as new and emmerging cells
- deleted data are displayed as shrinking and removed cells
- evolving overall shape is possible
-
Real life use cases
- Democratic Primaries: Preferential Poll Results by Nadieh Bremer for Swayable (more details at https://www.visualcinnamon.com/portfolio/swayable-preferential-polling); in reality, this real life use case does not use the plugin, but it was the premice of this plugin
- Democratic Primaries: Preferential Poll Results by Nadieh Bremer for Swayable (more details at https://www.visualcinnamon.com/portfolio/swayable-preferential-polling); in reality, this real life use case does not use the plugin, but it was the premice of this plugin
-
Examples with available code
<script src="https://d3js.org/d3.v6.min.js"></script>
<script src="https://rawcdn.githack.com/Kcnarf/d3-weighted-voronoi/v1.1.3/build/d3-weighted-voronoi.js"></script>
<script src="https://rawcdn.githack.com/Kcnarf/d3-voronoi-map/v2.1.1/build/d3-voronoi-map.js"></script>
<script src="https://rawcdn.githack.com/Kcnarf/d3-voronoi-treemap/v0.0.1/build/d3-voronoi-map-tween.js">
<script>
var voronoiMapTween = d3.voronoiMapTween();
</script>
In your javascript, in order to define the tween:
var startingVoronoiMapSimulation = d3.voronoiMapSimulation(startingData);
goToFinalState(startingVoronoiMapSimulation); // get the most representative Voronoï map, using d3-voronoi-map's *static* computation feature
var endingVoronoiMapSimulation = d3.voronoiMapSimulation(endingData);
goToFinalState(endingVoronoiMapSimulation); // get the most representative Voronoï map, using d3-voronoi-map's *static* computation feature
var voronoiMapTween = d3.voronoiMapTween(startingVoronoiMapSimulation, endingVoronoiMapSimulation);
var voronoiMapInterpolator = voronoiMapTween.mapInterpolator(); // interpolator of the Voronoi maps
Then, later in your javascript, in order to compute the interpolated Voronoï map cells, set the desired interpolation value (within [0, 1]
):
var interpolatedVoronoiMapCells = voronoiMapTween(0.5); // basic use case, returns a set of polygons/cells
var startingVoronoiMapCells = voronoiMapTween(0); // at 0, similar to startingVoronoiMap.state().polygons
var endingVoronoiMapCells = voronoiMapTween(1); // at 1, similar to endingVoronoiMap.state().polygons
# d3.voronoiMapTween(startingVoronoiMapSimluation, endingVoronoiMapSimluation)
Creates a new voronoiMapTween based on the two d3-voronoi-map simulations, and with the default configuration values and functions (startingKey, endingKey, clipInterpolator).
# voronoiMapTween.mapInterpolator()
Returns a function which is the interpolator between the starting Voronoï map and the ending Voronoï map. Calling mapInterpolator(interpolationValue) returns a Voronoï map, which is a sparse array of polygons, one for each data coming from either the starting data set or the ending data set. The interpolation value must be a float value within [0, 1]
:
mapInterpolator(0)
returns a Voronoï map similar* tostartingVoronoiMapSimluation.state().polygons
; similar* means same polygons, but not necessarily in the same order; there is no polygon for data exclusively in the endingVoronoiMapSimluationmapInterpolator(1)
returns a Voronoï map similar* toendingVoronoiMapSimluation.state().polygons
; there is no polygon for data exclusively in the startingVoronoiMapSimluation- else, returns an intermediate Voronoï map inbetween the starting and ending Voronoï maps for any other value within
]0,1[
For each computed polygon p
, p.site.originalObject
gives access to the interpolated site and its caracteristics:
p.site.originalObject.key
is the key, retrieved from either startingKey or endingKey, and which allows to make the correspondance between starting and ending datap.site.originalObject.interpolatedX
andp.site.originalObject.interpolatedY
are its interpolate coordinatesp.site.originalObject.interpolatedDataWeight
is the interpolated weight of the underlying datap.site.originalObject.tweenType
, in[ENTER_TWIN_TYPE, UPDATE_TWEEN_TYPE, EXIT_TWEEN_TYPE]
, defines if the site handles a entering/updating/exiting datap.site.originalObject.startingData
andp.site.endingData
reference the starting and ending data; one of the two references may be null if the site corresponds to a datum only available in the starting data set or only in the ending data set
# voronoiMapTween.startingKey([key])
In order to make the correspondance between the starting and ending cells of a single datum, each starting cell is assigned a key, retrieved from its underlying datum throught the starting key accessor. The starting key accessor and the ending key accessor may be distincts.
If key is specified, sets the key accessor, which must be a function accepting a parameter wich reference a datum (i.e. a element of the starting data set used to compute the starting Voronoï map). If key is not specified, returns the current key accessor, which defaults to:
function key(d) {
return d.id;
}
# voronoiMapTween.endingKey([key])
Same as startingKey, but for the ending cells.
# voronoiMapTween.clipInterpolator([ƒ])
If ƒ is specified, sets the clipping polygon interpolator. If ƒ is not specified, returns the current interpolator, which defaults to:
function ƒ(interpolationValue) {
return startingVoronoiMapSimulation.clip();
}
By default, we consider the starting and ending Voronoï maps having the same clipping polygon (thus, the default clipInterpolator interpolates nothing ;-). When the clipping polygon evolves, this API should be used to provide the clipping polygon interpolator, which must be a function ƒ accepting a float parameter in [0, 1]
where:
ƒ(0)
returns the starting clipping polygonƒ(1)
returns the ending clipping polygon- else returns an intermediate polygon inbetween the satrting and ending polygon for any other value within
]0,1[
As a simple first example, if the starting and ending clipping polygons are squares of different sizes, the clipInterpolator may look like:
const startingSize = 50;
const endingSize = 100;
function ƒ(interpolationValue) {
const intermediateSize = (1 - interpolationValue) * startingSize + interpolationValue * endingSize; // lerp interpolation
return [
[0, 0],
[0, intermediateSize],
[intermediateSize, intermediateSize],
[intermediateSize, 0],
];
}
// f(0) returns [[0,0], [0,50], [50,50], [50,0]]
// f(1) returns [[0,0], [0,100], [100,100], [100,0]]
// f(0.5) returns [[0,0], [0,75], [75,75], [75,0]]
voronoiMapTween.clipInterpolator(ƒ);
Note: if the starting and ending clipping polygons are of the same kind (e.g. a square, a disc) but with distinct sizes (as in the above example), you can try to use a static clipping polygon, and then scale the svg/paths.
As a second example, for more complexe use cases where the starting and ending shapes are not of the same kind (e.g. a circle and a pentagon), you can provide a clipInterpolator using flubber:
const startingClippingPolygon = [...]; // an array of 2D points, ordered counterclockwise, defining a convex shape
const endingClippingPolygon = [...]; // another array of 2D points
const ƒ = flubber.interpolate(startingClippingPolygon, endingClippingPolygon, {string: false}); // {string:false} produces an array of 2D points
voronoiMapTween.clipInterpolator(ƒ);
- d3-voronoi-map.voronoiMapSimulation
d3-voronoi-map-tween attempts to follow semantic versioning and bump major version only when backward incompatible changes are released.