aleksbobic / d3-voronoi-map

D3 plugin which computes a map (one-level treemap) based on a Voronoi tesselation

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

d3-voronoi-map

This D3 plugin produces a Voronoï map (i.e. one-level treemap). Given a convex polygon and weighted data, it tesselates/partitions the polygon in several inner cells, such that the area of a cell represents the weight of the underlying datum.

Because a picture is worth a thousand words:

square hexagon diamond circle

Available for d3 v4 and d3 v5.

If you're interested on multi-level treemap, which handle nested/hierarchical data, take a look at the d3-voronoi-treemap plugin.

Context

D3 already provides a d3-treemap module which produces a rectangular treemap. Such treemaps could be distorted to fit shapes that are not rectangles (cf. Distorded Treemap - d3-shaped treemap).

This plugin allows to compute a map with a unique look-and-feel, where inner areas are not strictly aligned each others, and where the outer shape can be any hole-free convex polygons (squares, rectangles, pentagon, hexagon, ... any regular convex polygon, and also any non regular hole-free convex polygon).

The drawback is that the computation of a Voronoï map is based on a iteration/looping process. Hence, it requires some times, depending on the number and type of data/weights, the desired representativeness of cell areas.

Examples

Installing

Load https://rawgit.com/Kcnarf/d3-voronoi-map/master/build/d3-voronoi-map.js (or its d3-voronoi-map.min.js version) to make it available in AMD, CommonJS, or vanilla environments. In vanilla, you must load the d3-weighted-voronoi plugin prior to this one, and a d3 global is exported:

<script src="https://d3js.org/d3.v4.min.js"></script>
<script src="https://raw.githack.com/Kcnarf/d3-weighted-voronoi/master/build/d3-weighted-voronoi.js"></script>
<script src="https://raw.githack.com/Kcnarf/d3-voronoi-map/master/build/d3-voronoi-map.js"></script>
<script>
  var voronoiMap = d3.voronoiMap();
</script>

TL;DR;

In your javascript, in order to define the tessellation:

var voronoiMap = d3.voronoiMap()
  .weight(function(d){ return weightScale(d); }         // set the weight accessor
  .clip([[0,0], [0,height], [width, height], [width,0]])  // set the clipping polygon

var res = voronoiMap(data);                         // compute the weighted Voronoi tessellation; returns {polygons, iterationCount, convergenceRatio}
var cells = res.polygons

Then, later in your javascript, in order to draw cells:

d3.selectAll('path')
  .data(cells)
  .enter()
  .append('path')
  .attr('d', function(d) {
    return cellLiner(d) + 'z';
  })
  .style('fill', function(d) {
    return fillScale(d.site.originalObject);
  });

Reference

API

# d3.voronoiMap()

Creates a new voronoiMap with the default accessors and configuration values (weight, clip, convergenceRatio, maxIterationCount, minWeightRatio, initialPosition, and initialWeight).

# voronoiMap(data)

Computes the Voronoï map for the specified data weights.

Returns a hash where hash.polygons is a sparse array of polygons clipped to the clip-ping polygon, one for each cell (each unique input point) in the diagram. Each polygon is represented as an array of points [x, y] where x and y are the point coordinates, a site field that refers to its site (ie. with x, y and weight retrieved from the original data), and a site.originalObject field that refers to the corresponding element in data. Polygons are open: they do not contain a closing point that duplicates the first point; a triangle, for example, is an array of three points. Polygons are also counterclockwise (assuming the origin ⟨0,0⟩ is in the top-left corner). Furthermore, hash.iterationCount is the number of iterations required to compute the resulting map, and hash.convergenceRatio is the final convergence ratio (ie. cell area errors / area of the clip-ping polygon).

# voronoiMap.weight([weight])

If weight-accessor is specified, sets the weight accessor. If weight is not specified, returns the current weight accessor, which defaults to:

function weight(d) {
  return d.weight;
}

# voronoiMap.clip([clip])

If clip is specified, sets the clipping polygon, compute the adequate extent and size, and returns this layout. clip defines a hole-free convex polygon, and is specified as an array of 2D points [x, y], which must be (i) open (no duplication of the first D2 point) and (ii) counterclockwise (assuming the origin ⟨0,0⟩ is in the top-left corner). If clip is not specified, returns the current clipping polygon, which defaults to:

[[0, 0], [0, 1], [1, 1], [1, 0]];

# voronoiMap.extent([extent])

If extent is specified, it is a convenient way to define the clipping polygon as a rectangle. It sets the extent, computes the adequate clipping polygon and size, and returns this layout. extent must be a two-element array of 2D points [x, y], which defines the clipping polygon as a rectangle with the top-left and bottom-right corners respectively set to the first and second points (assuming the origin ⟨0,0⟩ is in the top-left corner on the screen). If extent is not specified, returns the current extent, which is [[minX, minY], [maxX, maxY]] of current clipping polygon, and defaults to:

[[0, 0], [1, 1]];

# voronoiMap.size([size])

If size is specified, it is a convenient way to define the clipping polygon as a rectangle. It sets the size, computes the adequate clipping polygon and extent, and returns this layout. size must be a two-element array of numbers [width, height], which defines the clipping polygon as a rectangle with the top-left corner set to [0, 0]and the bottom-right corner set to [width, height](assuming the origin ⟨0,0⟩ is in the top-left corner on the screen). If size is not specified, returns the current size, which is [maxX-minX, maxY-minY] of current clipping polygon, and defaults to:

[1, 1];

# voronoiMap.convergenceRatio([convergenceRatio])

If convergenceRatio is specified, sets the convergence ratio, which stops computation when (cell area errors / clipping polygon area) <= convergenceRatio. If convergenceRatio is not specified, returns the current convergenceRatio , which defaults to:

var convergenceRation = 0.01; // stops computation when cell area error <= 1% clipping polygon's area

The smaller the convergenceRatio, the more representative is the map, the longer the computation takes time.

# voronoiMap.maxIterationCount([maxIterationCount])

If maxIterationCount is specified, sets the maximum allowed number of iterations, which stops computation when it is reached, even if the convergenceRatio is not reached. If maxIterationCount is not specified, returns the current maxIterationCount , which defaults to:

var maxIterationCount = 50;

If you want to wait until computation stops only when the convergenceRatio is reached, just set the maxIterationCount to a large amount. Be warned that computation may take a huge amount of time, due to flickering behaviours in later iterations.

# voronoiMap.minWeightRatio([minWeightRatio])

If minWeightRatio is specified, sets the minimum weight ratio, which allows to compute the minimum allowed weight (= maxWeight * minWeightRatio). If minWeightRatio is not specified, returns the current minWeightRatio , which defaults to:

var minWeightRatio = 0.01; // 1% of maxWeight

minWeightRatio allows to mitigate flickerring behaviour (caused by too small weights), and enhances user interaction by not computing near-empty cells.

# voronoiMap.initialPosition([initialPosition])

If initialPosition is specified, sets the initial coordinate accessor. The accessor is a callback wich is passed the datum, its index, the array it comes from, and the current d3-voronoi-map. The accessor must provide an array of two numbers [x, y] inside the clipping polygon, otherwise a random initial position is used instead. If initialPosition is not specified, returns the current accessor, which defaults to a random position inside the clipping polygon:

function randomInitialPosition(d, i, arr, voronoiMap) {
  var clippingPolygon = voronoiMap.clip(),
    extent = voronoiMap.extent(),
    minX = extent[0][0],
    maxX = extent[1][0],
    minY = extent[0][1],
    maxY = extent[1][1],
    dx = maxX - minX,
    dy = maxY - minY;
  var x, y;

  x = minX + dx * Math.random();
  y = minY + dy * Math.random();
  while (!polygonContains(clippingPolygon, [x, y])) {
    x = minX + dx * Math.random();
    y = minY + dy * Math.random();
  }
  return [x, y];
}

Above is a quite complex accessor that uses the current d3-voronoi-map's API to ensure that sites are positioned inside the clipping polygon, but the accessor may be simpler (-:

function precomputedInitialPosition(d, i, arr, voronoiMap) {
  return [d.precomputedX, d.precomputedY];
}

Considering the same set of data, severall Voronoï map computations lead to disctinct final arrangements, due to the default random initial position of sites. If initialPosition is a callback producing repeatable results, then several computations produce the same final arrangement. This is useful if you want the same arrangement for distinct page loads/reloads.

# voronoiMap.initialWeight([initialWeight])

If initialWeight is specified, sets the initial weight accessor. The accessor is a callback wich is passed the datum, its index, the array it comes from, and the current d3-voronoi-map. The accessor must provide a positive amount. If initialWeight is not specified, returns the current accessor, which defaults to initialize all sites with the same amount (which depends on the clipping polygon and the number of data):

function halfAverageAreaInitialWeight(d, i, arr, vornoiMap) {
  var siteCount = arr.length,
    totalArea = d3PolygonArea(vornoiMap.clip());

  return totalArea / siteCount / 2; // half of the average area of the the clipping polygon
}

Above is a quite complex accessor that uses the current d3-voronoi-map's API that sets the same weight for all sites, but the accessor may be simpler (-:

function precomputedInitialWeight(d, i, arr, voronoiMap) {
  return d.precomputedWeight;
}

Considering a unique clipping polygon where you want to animate the same data but with slightly different weights (e.g., animate according to the time), this API combined with the initialPosition API allows you to maintain areas from one set to another:

  • first, compute the Voronoï map of a first set of data
  • then, compute the Voronoï map of another set of data, by initilizing sites to the final values (positions and weights) of first Voronoï map

Dependencies

  • d3-polygon.{polygonCentroid, polygonArea, polygonContains}
  • d3-weighted-voronoi.weightedVoronoi

Testing

In order to test the code

git clone https://github.com/Kcnarf/d3-voronoi-map.git
[...]
yarn install
[...]
yarn test

About

D3 plugin which computes a map (one-level treemap) based on a Voronoi tesselation

License:BSD 3-Clause "New" or "Revised" License


Languages

Language:JavaScript 100.0%