Voronoi Treemaps are methods of visualizing hierarchical data, where each cell represents a portion of the data and the hierarchy is encoded by the nesting of cells within each other. Unlike traditional Treemaps, Voronoi Tree Maps do not use rectangles to represent the data, but rather use arbitrary polygons to represent the data. This allows for a more organic and natural representation of the data.
This is a Traditional Treemap. Notice how the rectangles are used to represent the data. This can lead to a more rigid and less organic representation of the data.
This is a Voronoi Treemap representing the GDP of countries in asia. Notice how the polygons are used to represent the data. As you can see the data seems to have a more organic and natural representation.
In order to create a simple Voronoi treemap using D3.js, follow these steps:
Set up the HTML structure with an SVG container where the treemap will be rendered.
<svg id="simpleVoronoi"></svg>
The <svg> element with the ID "simpleVoronoi"
serves as the container for the Voronoi treemap.
Import the D3.js library by adding the following script tag just before your custom script:
<script
src="https://d3js.org/d3.v7.min.js"></script>
This script tag imports the D3.js library from the official CDN, making D3.js functionalities available in your project.
Define constants for SVG dimensions, data, and necessary variables. Initialize the Voronoi treemap by calling functions to set up data, layout, and drawing.
const HEIGHT = 500;
const WIDTH = 960;
const HALF_WIDTH = WIDTH / 2;
const HALF_HEIGHT = HEIGHT / 2;
// Define your data array here
const data = [
{ name: "China", weight: 46.0, color: "#ffffcc" },
{ name: "Japan", weight: 18.32, color: "#ffcc99" },
{ name: "India", weight: 8.77, color: "#ffcccc" },
{ name: "South Korea", weight: 5.77, color: "#ff99cc" },
{ name: "Russia", weight: 5.58, color: "#ffccff" },
{ name: "Indonesia", weight: 3.6, color: "#cc99ff" },
{ name: "Turkey", weight: 3.01, color: "#ccccff" },
{ name: "Saudi Arabia", weight: 2.7, color: "#99ccff" },
{ name: "Iran", weight: 1.77, color: "#ccffff" },
{ name: "Thaïland", weight: 1.64, color: "#99ffcc" },
{ name: "UAE", weight: 1.55, color: "#ccffcc" },
{ name: "Hong Kong", weight: 1.3, color: "#ccff99" },
];
// Initialize SVG container
const svg = d3.select("#simpleVoronoi")
.attr("width", WIDTH)
.attr("height", HEIGHT);
const TREEMAP_RADIUS = Math.min(HALF_WIDTH, HALF_HEIGHT);
const _voronoiTreemap = d3.voronoiTreemap();
let hierarchy, circlingPolygon;
const fontScale = d3.scaleLinear();
function init(rootData) {
initData();
initLayout();
hierarchy = d3.hierarchy({ children: rootData }).sum((d) => d.weight);
_voronoiTreemap.clip(circlingPolygon)(hierarchy);
drawTreemap(hierarchy);
}
init(data);
This block of code initializes constants, sets up the SVG container, and prepares data and layout for the Voronoi treemap.
Constants like HEIGHT and WIDTH define the
dimensions of the SVG container. Data, such as country names,
weights, and colors, are stored in the data array. The
init function initializes the treemap by calling other
functions to set up data, layout, and drawing.
Define functions to initialize data and layout settings for the treemap.
function initData() {
circlingPolygon = computeCirclingPolygon();
fontScale.domain([3, 20]).range([8, 20]).clamp(true);
}
function computeCirclingPolygon() {
return [
[0, 0],
[WIDTH, 0],
[WIDTH, HEIGHT],
[0, HEIGHT],
];
}
function initLayout() {
const drawingArea = svg.append("g").classed("drawingArea", true);
const treemapContainer = drawingArea.append("g").classed("treemap-container", true);
treemapContainer
.append("path")
.classed("world", true)
.attr("transform", `translate(${-TREEMAP_RADIUS}, ${-TREEMAP_RADIUS})`)
.attr("d", `M${circlingPolygon.join(",")}Z`);
}
These functions initialize data and layout settings for the treemap, including the circling polygon and font scale.
The initData function calculates the circling polygon
and sets the font scale based on the provided data. The
computeCirclingPolygon function calculates the vertices
of the circling polygon, while the initLayout function
sets up the SVG container and draws the circling polygon.
Create a function to draw the Voronoi treemap based on the provided data and layout.
function drawTreemap(hierarchy) {
const leaves = hierarchy.leaves();
const cells = svg.select(".treemap-container")
.append("g")
.classed("cells", true)
.selectAll(".cell")
.data(leaves)
.enter()
.append("path")
.classed("cell", true)
.attr("d", (d) => `M${d.polygon.join(",")}z`)
.style("stroke", "black")
.style("stroke-width", "10px")
.style("fill", (d) => d.data.color);
const labels = svg.select(".treemap-container")
.append("g")
.classed("labels", true)
.selectAll(".label")
.data(leaves)
.enter()
.append("g")
.classed("label", true)
.attr("transform", (d) => `translate(${d.polygon.site.x}, ${d.polygon.site.y})`)
.style("font-size", (d) => fontScale(d.data.weight));
labels
.append("text")
.classed("name", true)
.html((d) => d.data.name);
labels
.append("text")
.classed("value", true)
.text((d) => `${d.data.weight}%`);
}
This function draws the Voronoi treemap based on the provided data and layout settings, including cells and labels.
The drawTreemap function creates cells and labels for
each data point in the treemap. It appends SVG elements for cells
and labels, styles them accordingly, and positions them based on the
calculated data. The cells represent the Voronoi regions, while the
labels display country names and weights.
You will notice in the tutorials that there are multiple functions that are not included in d3, that is because the operations necessary for the Voronoi treemap are imported via the d3-voronoi-treemap plugin. The documentation and installation instructions of which can be found using this link: here. This plugin hosts the relevant function calls to build a visualization of this model.
d3.voronoiTreemap() creates a new voronoiTreemap with a
set of default configuration values, functions, and algorithms,
including clip, extent, size,
convergenceRatio, maxIterationCount,
minWeightRatio, and prng. Some of these
standard methods are used in the tutorial and are fairly
self-explanatory; they all play their part in the process for
creating a Voronoi treemap.
These methods are essential for configuring various aspects of the Voronoi treemap, such as the size, convergence behavior, and clipping boundaries. Understanding and properly configuring these parameters are crucial for effectively visualizing data using the Voronoi treemap technique.
A voronoi diagram starts with a set of 2d coordinate points plotted on a plane.
Run a Delaunay triangulation on the set of two-dimensional points
Join the circumcenters of each triangle to get the voronoi diagram
More details on the Voronoi diagram can be found here.
This code sets up a module that can be used in both CommonJS and AMD (Asynchronous Module Definition) environments. It handles dependency injection, allowing the module to work with various module systems. The module takes exports and d3VoronoiMap as dependencies. It then passes these dependencies to the factory function.
_voronoiTreemap is the main function representing the
Voronoi treemap algorithm. Additional functions are defined on
_voronoiTreemap to get or set various parameters such as
convergence ratio, max iteration count, etc.
recurse is a private function used internally to generate
the Voronoi treemap recursively. It assigns the clipping polygon to
the node and computes the Voronoi map for its children recursively.
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('d3-voronoi-map')) :
typeof define === 'function' && define.amd ? define(['exports', 'd3-voronoi-map'], factory) :
(factory((global.d3 = global.d3 || {}),global.d3));
}(this, function (exports,d3VoronoiMap) { 'use strict';
function voronoiTreemap() {
//begin: constants
var DEFAULT_CONVERGENCE_RATIO = 0.01;
var DEFAULT_MAX_ITERATION_COUNT = 50;
var DEFAULT_MIN_WEIGHT_RATIO = 0.01;
var DEFAULT_PRNG = Math.random;
//end: constants
/////// Inputs ///////
var clip = [
[0, 0],
[0, 1],
[1, 1],
[1, 0],
]; // clipping polygon
var extent = [
[0, 0],
[1, 1],
]; // extent of the clipping polygon
var size = [1, 1]; // [width, height] of the clipping polygon
var convergenceRatio = DEFAULT_CONVERGENCE_RATIO; // targeted allowed error ratio; default 0.01 stops computation when cell areas error <= 1% clipping polygon's area
var maxIterationCount = DEFAULT_MAX_ITERATION_COUNT; // maximum allowed iteration; stops computation even if convergence is not reached; use a large amount for a sole converge-based computation stop
var minWeightRatio = DEFAULT_MIN_WEIGHT_RATIO; // used to compute the minimum allowed weight; default 0.01 means 1% of max weight; handle near-zero weights, and leaves enought space for cell hovering
var prng = DEFAULT_PRNG; // pseudorandom number generator
//begin: internals
var unrelevantButNeedeData = [
{
weight: 1,
},
{
weight: 1,
},
];
var _convenientReusableVoronoiMapSimulation = d3VoronoiMap.voronoiMapSimulation(unrelevantButNeedeData).stop();
//end: internals
///////////////////////
///////// API /////////
///////////////////////
function _voronoiTreemap(rootNode) {
recurse(clip, rootNode);
}
_voronoiTreemap.convergenceRatio = function (_) {
if (!arguments.length) {
return convergenceRatio;
}
convergenceRatio = _;
return _voronoiTreemap;
};
_voronoiTreemap.maxIterationCount = function (_) {
if (!arguments.length) {
return maxIterationCount;
}
maxIterationCount = _;
return _voronoiTreemap;
};
_voronoiTreemap.minWeightRatio = function (_) {
if (!arguments.length) {
return minWeightRatio;
}
minWeightRatio = _;
return _voronoiTreemap;
};
_voronoiTreemap.clip = function (_) {
if (!arguments.length) {
return clip;
}
//begin: use voronoiMap.clip() to handle clip/extent/size computation and borderline input (non-counterclockwise, non-convex, ...)
_convenientReusableVoronoiMapSimulation.clip(_);
//end: use voronoiMap.clip() to handle clip/extent/size computation
clip = _convenientReusableVoronoiMapSimulation.clip();
extent = _convenientReusableVoronoiMapSimulation.extent();
size = _convenientReusableVoronoiMapSimulation.size();
return _voronoiTreemap;
};
_voronoiTreemap.extent = function (_) {
if (!arguments.length) {
return extent;
}
//begin: use voronoiMap.extent() to handle clip/extent/size computation
_convenientReusableVoronoiMapSimulation.extent(_);
//end: use voronoiMap.clip() to handle clip/extent/size computation
clip = _convenientReusableVoronoiMapSimulation.clip();
extent = _convenientReusableVoronoiMapSimulation.extent();
size = _convenientReusableVoronoiMapSimulation.size();
return _voronoiTreemap;
};
_voronoiTreemap.size = function (_) {
if (!arguments.length) {
return size;
}
//begin: use voronoiMap.size() to handle clip/extent/size computation
_convenientReusableVoronoiMapSimulation.size(_);
//end: use voronoiMap.clip() to handle clip/extent/size computation
clip = _convenientReusableVoronoiMapSimulation.clip();
extent = _convenientReusableVoronoiMapSimulation.extent();
size = _convenientReusableVoronoiMapSimulation.size();
return _voronoiTreemap;
};
_voronoiTreemap.prng = function (_) {
if (!arguments.length) {
return prng;
}
prng = _;
return _voronoiTreemap;
};
///////////////////////
/////// Private ///////
///////////////////////
function recurse(clippingPolygon, node) {
var simulation;
//assign polygon to node
node.polygon = clippingPolygon;
if (node.height != 0) {
//compute one-level Voronoi map of children
simulation = d3VoronoiMap.voronoiMapSimulation(node.children)
.clip(clippingPolygon)
.weight(function (d) {
return d.value;
})
.convergenceRatio(convergenceRatio)
.maxIterationCount(maxIterationCount)
.minWeightRatio(minWeightRatio)
.prng(prng)
.stop();
var state = simulation.state(); // retrieve the Voronoï map simulation's state
//begin: manually launch each iteration until the Voronoï map simulation ends
while (!state.ended) {
simulation.tick();
state = simulation.state();
}
//end: manually launch each iteration until the Voronoï map simulation ends
//begin: recurse on children
state.polygons.forEach(function (cp) {
recurse(cp, cp.site.originalObject.data.originalData);
});
//end: recurse on children
}
}
return _voronoiTreemap;
}
exports.voronoiTreemap = voronoiTreemap;
Object.defineProperty(exports, '__esModule', { value: true });
}));