This block experiments weighted Voronoï diagram (the 2D additive weighted power diagram variation) with many sites. This is a continuation of 2 and 1.
This is still a Work In Progress, as the code is not yet packaged/self-contained, and contains weird hacks. For a more up-to-date version of the code, please refer to the Weighted Voronoi Treemap in D3v4 block.
xxxxxxxxxx
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Voronoï playground : interactive weighted Voronoï study III (multiple sites)</title>
<meta name="description" content="Weighted Voronoï in D3.js">
<script src="https://d3js.org/d3.v4.min.js" charset="utf-8"></script>
<script language="javascript" type="text/javascript" src="ConvexHull.js"></script>
<script language="javascript" type="text/javascript" src="PowerDiagram.js"></script>
<script language="javascript" type="text/javascript" src="d3-polygon-clip.js"></script>
<style>
#wip {
display: none;
position: absolute;
top: 200px;
left: 330px;
font-size: 40px;
text-align: center;
}
.control {
position: absolute;
top: 5px;
}
.control#control-0 {
left: 5px;
}
.control#control-1 {
right: 5px;
}
.control input {
width: 400px;
}
#drawing-area.dragging {
cursor: move;
}
#site-container {
clip-path: url("#clipper");
}
.seed {
fill: steelBlue;
}
.seed.dragging, .seed:hover {
fill: pink;
cursor: move;
}
.seed.group-green {
fill: green;
}
.seed.group-green.dragging, .seed.group-green:hover {
fill: pink;
cursor: move;
}
.unit-circle {
fill: none;
stroke: lightsteelBlue;
}
.unit-circle.group-green {
stroke: lightgreen;
}
.cell {
fill-opacity: 0.1;
fill: lightsteelBlue;
stroke: lightsteelBlue;
}
.cell.group-green {
fill: lightgreen;
stroke: lightgreen;
}
</style>
</head>
<body>
<div id="control-0" class="control">
Blue sites' weight :
<form>
<input id="weight" type="range" name="points" min="-10000" max="100000" value="0" oninput="weightUpdated()">
</form>
</div>
<div id="control-1" class="control">
Green sites' weight :
<form>
<input id="weight" type="range" name="points" min="-10000" max="100000" value="0" oninput="weightUpdated()">
</form>
</div>
<svg>
<defs>
<clipPath id="clipper">
<rect x="0" y="0" width="960" height="500" />
</clipPath>
</defs>
<g id="drawing-area">
<g id="cell-container"></g>
<g id="site-container"></g>
</g>
</svg>
<div id="wip">
Work in progress ...
</div>
</body>
<script>
var WITH_TRANSITION = true;
var WITHOUT_TRANSITION = false;
var duration = 250;
//begin: layout conf.
var svgWidth = 960,
svgHeight = 500,
margin = {top: 40, right: 10, bottom: 10, left: 10},
width = svgWidth - margin.left - margin.right,
height = svgHeight - margin.top - margin.bottom,
halfWidth = width/2,
halfHeight = height/2,
quarterWidth = width/4,
quarterHeight = height/4;
//end: layout conf.
//begin: voronoi stuff definitions
var blueSites = [{index: 0, group: "blue", x: halfWidth-100, y: halfHeight, weight: 0},
{index: 1, group: "blue", x: halfWidth+100, y: halfHeight, weight:0}],
greenSites = [{index: 2, group: "green", x: halfWidth-epsilon, y: halfHeight-100, weight:0},
{index: 3, group: "green", x: halfWidth, y: halfHeight+100, weight:0}],
sites = blueSites.concat(greenSites);
var clippingPolygon = [[0,0], [0,height], [width,height], [width,0]];
var cells = sites.map(function(s){ return []; }); // stores, for each site, each cell's verteces
//end: voronoi stuff definitions
//begin: utilities
function uc(w) { return (w<=0)? 0 : Math.sqrt(w); } //scale for unit-circles
var cellLiner = d3.line()
.x(function(d){ return d[0]; })
.y(function(d){ return d[1]; });
//end: utilities
//begin: reusable d3-selections
var svg = d3.select("svg"),
clipper = d3.select("#clipper>rect"),
drawingArea = d3.select("#drawing-area"),
cellContainer = d3.select("#cell-container"),
siteContainer = d3.select("#site-container");
//end: reusable d3-selections
//begin: user interaction handlers
function weightUpdated() {
var newWeights = [],
newWeigth,
group;
newWeights[0] = +d3.select("#control-0 input").node().value;
newWeights[1] = +d3.select("#control-1 input").node().value;
if (newWeights[0] !== blueSites[0].weight) {
newWeight = newWeights[0];
group = blueSites;
} else {
newWeight = newWeights[1];
group = greenSites;
}
group.forEach(function(s){ s.weight = newWeight });
computeAllCells();
redrawAllCells(WITHOUT_TRANSITION);
redrawUnitCircleOfGroup(group, WITHOUT_TRANSITION);
redrawWeightsOfGroup(group);
}
var dragSite = d3.drag()
.subject(function(d) { return d; })
.container(drawingArea.node())
.on("start", dragStarted)
.on("drag", draggingSite)
.on("end", dragEnded);
function dragStarted(d) {
d3.select(this).classed("dragging", true);
drawingArea.classed("dragging", true);
}
function dragEnded(d) {
d3.select(this).classed("dragging", false);
drawingArea.classed("dragging", false);
}
function draggingSite(d) {
var newX = Math.round(d3.event.x),
newY = Math.round(d3.event.y);
if (newX!==d.x || newY!==d.y) {
d.x = newX;
d.y = newY;
computeAllCells();
redrawAllCells(WITHOUT_TRANSITION);
redrawSite(d,WITHOUT_TRANSITION);
}
}
//end: user interaction handlers
computeAllCells();
initLayout();
redrawAllCells(WITHOUT_TRANSITION);
redrawAllUnitCircles(WITHOUT_TRANSITION);
redrawAllWeights();
redrawAllSites(WITHOUT_TRANSITION);
/***************/
/* Computation */
/***************/
function computeAllCells() {
//begin: map sites to the expected format of PowerDiagram
var formatedSites = sites.map(function(s) {
return new Vertex(s.x, s.y, null, s.weight, s, false);
})
var boundingSites = getBoundingSites();
//end: map sites to the expected format of PowerDiagram
cells = computePowerDiagramIntegrated(formatedSites, boundingSites, clippingPolygon);
}
function getBoundingSites() {
var siteBasedBounding = false; // when activated (as done in original code), resulting clipping is weird: losange-shaped clipping
var boundingSites = [],
xExtent, yExtent,
minX, maxX, minY, maxY,
x0, x1, y0, y1;
if (siteBasedBounding) {
xExtent = d3.extent(sites.map(function(s) {return s.x;}));
yExtent = d3.extent(sites.map(function(s) {return s.y;}));
} else {
xExtent = [0,width];
yExtent = [0,height];
}
minX = xExtent[0];
maxX = xExtent[1];
minY = yExtent[0];
maxY = yExtent[1];
x0 = minX - maxX;
x1 = 2 * maxX;
y0 = minY - maxY;
y1 = 2 * maxY;
var result = [];
result[0] = [x0, y0];
result[1] = [x0, y1];
result[2] = [x1, y1];
result[3] = [x1, y0];
for (var i = 0; i < result.length; i++){
boundingSites.push( new Vertex(result[i][0], result[i][1], null, epsilon, new Vertex(result[i][0], result[i][1], null, epsilon, null, true), true));
}
return boundingSites;
}
/***********/
/* Drawing */
/***********/
function redrawAllSites(withTransition) {
sites.forEach(function(s){ redrawSite(s, withTransition); })
}
//redraw site = place site at adequate position
function redrawSite(site, withTransition) {
d3.select("#site-"+site.index).transition()
.duration(withTransition? duration : 0)
.attr("transform", function(d){ return "translate("+[d.x,d.y]+")"; });
}
function redrawAllWeights() {
sites.forEach(function(s){ redrawWeights(s); });
}
function redrawWeightsOfGroup(group) {
group.forEach(function(s){ redrawWeights(s); });
}
// redraw site's weight = update text
function redrawWeights(site) {
d3.select("#weight-"+site.index)
.text(function(d){ return "weight: " +d.weight; })
}
function redrawAllUnitCircles() {
sites.forEach(function(s){ redrawUnitCircle(s, WITHOUT_TRANSITION); });
}
function redrawUnitCircleOfGroup(group) {
group.forEach(function(s){ redrawUnitCircle(s, WITHOUT_TRANSITION); });
}
// redraw site's unit circle = update radius
function redrawUnitCircle(site, withTransition) {
d3.select("#unit-circle-"+site.index).transition()
.duration(withTransition? duration : 0)
.attr("r", function(d){ return uc(d.weight); })
}
function redrawAllCells(withTransition) {
var cellSelection = cellContainer.selectAll(".cell")
.data(cells, function(c){ return c.site.originalObject.index; });
cellSelection.enter()
.append("path")
.attr("class", function(d){ return "group-"+d.site.originalObject.group; })
.classed("cell", true)
.attr("id", function(d,i){ return "cell-"+d.site.originalObject.index; })
.merge(cellSelection)
.transition()
.duration(withTransition? duration : 0)
.attr("d", function(d){ return cellLiner(d)+"z"; });
cellSelection.exit().remove();
}
function initLayout () {
svg.attr("width", svgWidth)
.attr("height", svgHeight);
clipper.attr("x", 0)
.attr("y", 0)
.attr("width", width)
.attr("height", height);
drawingArea.attr("width", width)
.attr("height", height)
.attr("transform", "translate("+[margin.left, margin.top]+")");
//begin: draw sites
var drawnSites = siteContainer.selectAll(".site")
.data(sites)
.enter()
.append("g")
.attr("id", function(d){ return "site-"+d.index})
.classed("site", true);
drawnSites.append("circle")
.attr("id", function(d,i){ return "unit-circle-"+i; })
.attr("class", function(d){ return "group-"+d.group; })
.classed("unit-circle", true)
drawnSites.append("text")
.attr("id", function(d,i){ return "weight-"+i; })
.attr("transform", "translate("+[0,15]+")")
.attr("text-anchor", "middle");
drawnSites.append("circle")
.attr("id", function(d,i){ return "seed-"+i; })
.attr("class", function(d){ return "group-"+d.group; })
.classed("seed", true)
.attr("r", 4)
.call(dragSite);
//end: draw sites
//begin: draw cells
cellContainer.selectAll(".cell")
.data(cells)
.enter()
.append("path")
.attr("class", function(d){ return "group-"+d.site.originalObject.group; })
.classed("cell", true)
.attr("id", function(d,i){ return "cell-"+d.site.originalObject.index; });
//end: draw cells
}
</script>
</html>
https://d3js.org/d3.v4.min.js