viz = {
const w = width
const h = 300
const svg = d3.select(DOM.svg(w, h));
const svgRect = svg.append("rect")
.attr("width", w)
.attr("height", h)
svg
.append("path")
.attr("id", "hello")
.attr("d", testPathAbs)
.attr("fill", "none");
const centroid = d3.polygonCentroid(fishCoords)
svg.append("circle")
.attr("cx", centroid[0])
.attr("cy", centroid[1])
.attr("r", 4)
.attr("fill", "none")
const simulation = d3.forceSimulation(data)
.alphaMin(0.01)
.force('surface', function(){
data.map(function(d){
d.surface = Math.sqrt(
d.children.map(function(t){
return t.length * t.length;
})
.reduce(function(a,b){
return a + b;
}, 0) / d.children.length);
})
})
.force('colidecell', function(){colideCell()})
// .force('colidecell', function(){
// // cells.each(function(d){
// data.map(function(d){
// var p1 = d.r + buffer; //buffer for the current polygon
// //Co-ordinates of current polygon points
// d.polygon = d.children.map(function(t){
// return [(d.x + ((d.r * t.length) + buffer) * t.sin) , (d.y + ((d.r * t.length) + buffer) * t.cos)];
// // return [(d.x + d.r * t.length * t.sin), (d.y + d.r * t.length * t.cos)];
// });
// });
// var quadtree = d3.quadtree(data, d=>d.x, d=>d.y);
// var collisions = 0;
// //Loop through the cells, and shrink the polygon where it overlaps with neighbours
// // cells.each(function(d,i){
// data.map(function(d,i){
// quadtree.visit(function(node, x0, y0, x1, y1){
// var p = node.data;
// //Return quadtree nodes that contain any data.
// //Q: This confused me a little bit.
// //I thought the "!" would return nodes that DIDN'T contain any data.
// if (!p) return;
// //remove the current node
// if (p.id == d.id) return;
// //Calculate distance between current node and all others
// var dx = p.x - d.x,
// dy = p.y - d.y,
// dist2 = dx*dx + dy*dy;
// //Only keep nodes within a reasonable distance of current node
// if (dist2 > 4 * (d.r + p.r) * (d.r + p.r)) return;
// var stress = 0;
// //Polygon co-ordinates for current cell polygon
// d.children.forEach(function(t){
// //Co-ordinates for a single feeler
// var txy = [(d.x + ((d.r * t.length) + buffer) * t.sin), (d.y + ((d.r * t.length) + buffer) * t.cos)];
// var collisions = 0;
// //If the feeler overlaps neighbouring polygon
// if (d3.polygonContains(p.polygon, txy)) {
// collisions ++;
// stress++;
// //Bring feeler in a little bit
// // t.length /= 1.05;
// t.length /= 1.08;
// //Q: Could you please provide an overview of what this bit does?
// //I'm guessing this makes the overlapping polygons jiggle a bit more when
// //they overlap?
// var tens = d.surface / p.surface,
// f = 0.1 * (stress > 2 ? 1 : 0.5);
// // f = 0.1
// d.vx += f * Math.atan((d.x - p.x) * tens);
// d.vy += f * Math.atan((d.y - p.y) * tens);
// p.vx -= f * Math.atan((d.x - p.x) * tens);
// p.vy -= f * Math.atan((d.y - p.y) * tens);
// // console.log([d.vx,d.vy,p.vx,p.vy])
// }
// })
// })
// })
// })
.force("tension", function () {
cells.each(function (d) {
var l = d.children.length;
d.children.forEach(function (t, i) {
var m = d.children[(l - 1) % l].length + d.children[(l + 1) % l].length;
var f = 1 / 8; // spiky-ness
// var f = 1 / 10; // spiky-ness
t.length = (1 - f) * t.length + f * m / 2;
});
})
})
.force("internal", function (alpha) {
var f = 1 / 10;
cells.each(function (d) {
d.vx += f * d3.sum(d.children, function (t) {
return t.length * t.sin;
});
d.vy += f * d3.sum(d.children, function (t) {
return -t.length * t.cos;
});
})
})
.force("expand", function () {
cells.each(function (d) {
var u = 1 / Math.sqrt(d.surface);
d.children.forEach(function (t) {
t.length *= u;
t.length = Math.min(t.length, 1.1);
})
})
})
// .force('x', d3.forceX().strength(0.01).x(50))
.force('y', d3.forceY().strength(0.1).y(d => d.y - 10))
.force('shrink', function(){
const heights = data.map(d => d.y);
const upperQ = d3.quantile(heights.sort(d3.ascending), 0.65);
const lowerQ = d3.quantile(heights.sort(d3.ascending), 0.25);
cells.each(function(d){
if (d.y > upperQ | d.y < lowerQ) {d.color = 'green'} else {d.color = 'blue'};
d.children.forEach(function (t) {
if (d.y > upperQ | d.y < lowerQ) {t.length *= 0.75};
})
})
})
// .force('move', function(){
// cells.each(function(d){
// d.x += Math.random()-0.5;
// d.y += Math.random()-0.5;
// })
// })
.force('boundary', function(){
cells.each(function(d){
const pointCoords = [d.x, d.y]
if (!geometric.pointInPolygon(pointCoords, fishCoords)) {
d.color = 'red';
// Calculate the distance from the cell to the polygon's centroid
const distX = centroid[0] - d.x;
const distY = centroid[1] - d.y;
const distance = Math.sqrt(distX * distX + distY * distY);
// Calculate the force to pull the node back towards the centroid
const strength = 0.5; // Adjust the strength as needed
const forceX = (distX / distance) * strength;
const forceY = (distY / distance) * strength;
// Apply the force to the node
d.vx += forceX
d.vy += forceY
}
})
})
// Clip path-----------------------------------------------
const clipPath = svg
.append("defs")
.append("clipPath")
.attr("id", "clip-path-id");
clipPath
.append("path")
.attr("d", testPathAbs); // Replace with your SVG path data
// //Plot elements--------------------------------------------------
let cells = svg.append("g").attr("id","cellGroup")
// .attr('transform', 'translate(100,20)')
.selectAll('g.cell')
.data(simulation.nodes())
.enter()
.append('g')
.classed('cell', true)
.attr('transform', function (d) {
return 'translate(' + [d.x, d.y] + ')'
})
// .attr("opacity",0.5);
d3.select("#cellGroup").attr("clip-path", "url(#clip-path-id)");
let paths = cells
.append('path')
// .attr("fill", cellColor)
// .attr('fill', function (d,i){
// return d.color
// })
// .attr("opacity",0.6)
.attr('d', function (d) {
var arc = 2 * Math.PI / d.children.length,//Point spacing along circle
data = d.children
.map(function (t, i) {
return [i * arc, d.r * t.length];
})
return line(data);
})
.attr("stroke","black")
.attr("stroke-width",strokeWidth);
//plot feelers
// cells.each(function(d,i){
// let currentCell = d3.select(this);
// let data = d.children;
// currentCell.selectAll("circle")
// .data(data)
// .enter()
// .append("circle")
// .attr("cx",function(t){return (((d.r * t.length) + buffer) * t.sin)})
// .attr("cy",function(t){return (((d.r * t.length) + buffer) * t.cos)})
// .attr("r",1)
// .attr('fill', d.color)
// .attr("opacity",1)
// });
//Simulation iteration updates------------------------------------------
let tickcount = 0
simulation.on("tick",function(d) {
tickcount++
cells.attr('transform', function (d) {
return 'translate(' + [d.x, d.y] + ')'
});
// paths.attr('fill', function (d) {
// return d.color
// });
d3.select("#cellGroup").attr("clip-path", "url(#clip-path-id)");
paths.attr('d', function (d) {
var arc = 2 * Math.PI / d.children.length,//Point spacing along circle
data = d.children.map(function (t, i) {
return [i * arc, d.r * t.length];
});
return line(data);
});
//plot feelers
// cells.each(function(d){
// d3.select(this)
// .selectAll("circle")
// .attr("cx",function(t){return (((d.r * t.length) + buffer) * t.sin)})
// .attr("cy",function(t){return (((d.r * t.length) + buffer) * t.cos)})
// })
});
// return svg.node();
return Object.assign(svg.node(), {
update(backgroundCol, lineCol, cellCol, cellStroke) {
svgRect.attr("fill", backgroundCol);
d3.select("#hello").attr("stroke", lineCol);
paths.attr("fill", cellCol);
paths.attr("stroke", cellStroke);
}
})
}