|
<!DOCTYPE html> |
|
<meta charset="utf-8"> |
|
<style> |
|
|
|
.cell { |
|
pointer-events: all; |
|
fill: none; |
|
stroke: #666; |
|
stroke-opacity: 0.2; |
|
} |
|
|
|
.active circle { |
|
stroke: #000; |
|
stroke-width: 2px; |
|
} |
|
|
|
</style> |
|
<div id="control"> </div> |
|
<svg width="960" height="500"></svg> |
|
<script src="https://d3js.org/d3.v4.min.js"></script> |
|
<script> |
|
|
|
|
|
var svg = d3.select("svg"), |
|
width = +svg.attr("width"), |
|
height = +svg.attr("height"), |
|
radius = 32; |
|
|
|
var n = 0; |
|
var circles = d3.range(15).map(function() { |
|
return { |
|
n: n++, |
|
x: Math.round(Math.random() * (width - radius * 2) + radius), |
|
y: Math.round(Math.random() * (height - radius * 2) + radius) |
|
}; |
|
}); |
|
|
|
// control add/remove |
|
var addNew = false; |
|
d3.select('#control').append('input') |
|
.attr('type','button') |
|
.attr('value', addNew ? "Add" : "Remove") |
|
.on('click', function(d) { |
|
addNew = !addNew; |
|
d3.select(this).attr('value', addNew ? "Add" : "Remove") |
|
d3.selectAll('g').on('click', (addNew) ? add : remove); |
|
}); |
|
|
|
|
|
var color = d3.scaleOrdinal() |
|
.range(d3.schemeCategory20); |
|
|
|
var voronoi = d3.voronoi() |
|
.x(function(d) { return d.x; }) |
|
.y(function(d) { return d.y; }) |
|
.extent([[-1, -1], [width + 1, height + 1]]); |
|
|
|
var circle = svg.selectAll("g") |
|
.data(circles) |
|
.enter().append("g") |
|
.attr('id',function(d) { return 'g-'+d.n }) |
|
.call(d3.drag() |
|
.on("start", dragstarted) |
|
.on("drag", dragged) |
|
.on("end", dragended)) |
|
.on('click', (addNew) ? add : remove); |
|
|
|
var cell = circle.append("path") |
|
.data(voronoi.polygons(circles)) |
|
.attr("d", renderCell) |
|
.attr("class","cell") |
|
.attr("id", function(d) { return "cell-" + d.data.n; }); |
|
|
|
circle.append("clipPath") |
|
.attr("id", function(d) { return "clip-" + d.n; }) |
|
.append("use") |
|
.attr("xlink:href", function(d) { return "#cell-" + d.n; }); |
|
|
|
|
|
circle.append("circle") |
|
.attr("clip-path", function(d) { return "url(#clip-" + d.n + ")"; }) |
|
.attr("cx", function(d) { return d.x; }) |
|
.attr("cy", function(d) { return d.y; }) |
|
.attr("r", radius) |
|
.style("fill", function(d) { return color(d.n); }); |
|
|
|
circle.append("text") |
|
.attr("clip-path", function(d, i) { return "url(#clip-" + i + ")"; }) |
|
.attr("x", function(d) { return d.x; }) |
|
.attr("y", function(d) { return d.y; }) |
|
.attr("dy", '0.35em') |
|
.attr("text-anchor", function(d) { return 'middle'; }) |
|
.attr("opacity", 0.6) |
|
.style("font-size", "1.8em") |
|
.style("font-family", "Sans-Serif") |
|
.text(function(d) { return d.n; }) |
|
|
|
var simulation = d3.forceSimulation() |
|
.nodes(circles) |
|
.force('charge', d3.forceManyBody()); |
|
|
|
|
|
simulation.nodes(circles) |
|
.on('tick',ticked); |
|
|
|
|
|
function ticked() { |
|
circle.selectAll('circle') |
|
.attr("cx", function(d) { return d.x; }) |
|
.attr("cy", function(d) { return d.y; }) |
|
|
|
circle.selectAll('text') |
|
.attr("x", function(d) { return d.x; }) |
|
.attr("y", function(d) { return d.y; }); |
|
|
|
cell = cell.data(voronoi.polygons(circles)).attr("d", renderCell); |
|
|
|
} |
|
|
|
function dragstarted(d) { |
|
if (!d3.event.active) simulation.alphaTarget(0.3).restart(); |
|
d.fx = d.x; |
|
d.fy = d.y; |
|
} |
|
|
|
function dragged(d) { |
|
d3.select(this).select("circle").attr("cx", d.x = d3.event.x).attr("cy", d.y = d3.event.y); |
|
cell = cell.data(voronoi.polygons(circles)).attr("d", renderCell); |
|
d.fx = d3.event.x; |
|
d.fy = d3.event.y; |
|
|
|
|
|
} |
|
|
|
function dragended(d) { |
|
if (!d3.event.active) simulation.alphaTarget(0); |
|
d.fx = null; |
|
d.fy = null; |
|
} |
|
|
|
|
|
|
|
|
|
function remove () { |
|
|
|
d3.select(this).raise(); |
|
var id = d3.select(this).attr('id').split('-')[1]; |
|
id = +id; |
|
|
|
// Get the clicked item: |
|
var index = circles.map(function(d) { |
|
return d.n; |
|
}).indexOf(id); |
|
|
|
circles.splice(index,1); |
|
|
|
// Update circle data: |
|
var circle = svg.selectAll("g") |
|
.data(circles); |
|
|
|
circle.exit().remove(); |
|
circle.selectAll("clipPath").exit().remove(); |
|
circle.selectAll("circle").exit().remove(); |
|
circle.selectAll("text").exit().remove(); |
|
|
|
//// Update voronoi: |
|
d3.selectAll('.cell').remove(); |
|
cell = circle.append("path") |
|
.data(voronoi.polygons(circles)) |
|
.attr("d", renderCell) |
|
.attr("class","cell") |
|
.attr("id", function(d) { return "cell-" + d.data.n; }); |
|
|
|
simulation.nodes(circles) |
|
.on('tick',ticked); |
|
} |
|
|
|
function add() { |
|
// Add circle to circles: |
|
var coord = d3.mouse(this); |
|
var newIndex = d3.max(circles, function(d) { return d.n; }) + 1; |
|
circles.push({x: coord[0], y: coord[1], n: newIndex }); |
|
|
|
// Enter and Append: |
|
circle = svg.selectAll("g").data(circles).enter() |
|
|
|
var newCircle = circle.append("g") |
|
.attr('id',function(d) { return 'g-'+d.n }) |
|
.call(d3.drag() |
|
.on("start", dragstarted) |
|
.on("drag", dragged) |
|
.on("end", dragended)) |
|
.on('click',add) |
|
|
|
cell = circle.selectAll("path") |
|
.data(voronoi.polygons(circles)).enter(); |
|
|
|
cell.select('#g-'+newIndex).append('path') |
|
.attr("d", renderCell) |
|
.attr("class","cell") |
|
.attr("id", function(d) { console.log(d.data); return "cell-" + d.data.n; }); |
|
|
|
newCircle.data(circles).enter(); |
|
|
|
newCircle.append("clipPath") |
|
.attr("id", function(d) { return "clip-" + d.n; }) |
|
.append("use") |
|
.attr("xlink:href", function(d) { return "#cell-" + d.n; }); |
|
|
|
newCircle.append("circle") |
|
.attr("clip-path", function(d) { return "url(#clip-" + d.n + ")"; }) |
|
.attr("cx", function(d) { return d.x; }) |
|
.attr("cy", function(d) { return d.y; }) |
|
.attr("r", radius) |
|
.style("fill", function(d) { return color(d.n); }); |
|
|
|
newCircle.append("text") |
|
.attr("clip-path", function(d) { return "url(#clip-" + d.n + ")"; }) |
|
.attr("x", function(d) { return d.x; }) |
|
.attr("y", function(d) { return d.y; }) |
|
.attr("dy", '0.35em') |
|
.attr("text-anchor", function(d) { return 'middle'; }) |
|
.attr("opacity", 0.6) |
|
.style("font-size", "1.8em") |
|
.style("font-family", "Sans-Serif") |
|
.text(function(d) { return d.n; }) |
|
|
|
cell = d3.selectAll('.cell'); |
|
|
|
d3.select("#cell-"+newIndex).lower(); // ensure the path is above the circle in svg. |
|
|
|
simulation.nodes(circles) |
|
.on('tick',ticked); |
|
|
|
} |
|
|
|
function renderCell(d) { |
|
return d == null ? null : "M" + d.join("L") + "Z"; |
|
} |
|
|
|
</script> |