Published
Edited
Jun 23, 2021
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
data2 = Array.from( Array(16), (x,i)=>i+1).sort(() => Math.random() - 0.5)
Insert cell
Insert cell
JSON.stringify(data.map(d=>({x:d.x,y:d.y,r:d.r})),null,2)
Insert cell
metaballPacking={
const height=360;
const svg=d3.create("svg").attr("id","metaball-container").attr("viewBox", [0, 0, width, height]);
const g=svg.append('g').attr('transform','translate('+width/2+','+height/2+')');
const intersections=svg.append('g').attr('transform', 'translate('+width/2+','+height/2+')');
const circle=g.selectAll('cirle').data(data,d=>d.id)
.enter()
.append('circle')
.attr('fill','transparent')
.attr('stroke','blue')
.attr('r',d=>d.r)
.attr('cx',d=>d.x)
.attr('cy',d=>d.y);
const metaball=g.append('path')
.classed('metaball',true)
.attr('fill','none')
.attr('stroke','#e6ae55')
.attr('stroke-width',2)
.attr('d', drawMetaball(data, 50, intersections));
const text=g.selectAll('text').data(data,d=>d.id)
.enter()
.append('text')
.attr('x',d=>d.x)
.attr('y',d=>d.y+5)
.attr('text-anchor','middle')
.text(d=>d.id);
return svg.node()
}
Insert cell
function drawMetaball(data, sampleRate=50, intersections){
// Init the path
let metaball='';
// Return a simple circle in case we only have one circle
if (data.length===1)
{
metaball+=`
M ${data[0]['x']-data[0]['r']},${data[0]['y']}
a ${data[0]['r']},${data[0]['r']} 0 1, 0 ${data[0]['r']*2},0
a ${data[0]['r']},${data[0]['r']} 0 1, 0 ${-data[0]['r']*2},0
Z`;
metaball=metaball.replace(/\n/g,'').replace(/\s\s/g,'');
return metaball;
}
else if (data.length===2)
{
// Add "c" property (center) to all data elements
data.forEach(d=>d.c=[d.x,d.y]);
const pointsAndHandles1=curvesBetweenCircles(data[0].r, data[1].r, data[0].c, data[1].c);
const pointsAndHandles2=curvesBetweenCircles(data[1].r, data[0].r, data[1].c, data[0].c);
// set the starting point of the path
metaball+=`M ${pointsAndHandles1.p[1][0]},${pointsAndHandles1.p[1][1]} `;

// 1st Bezier Curve
metaball+=`
C ${pointsAndHandles1.h[1][0]},${pointsAndHandles1.h[1][1]}
${pointsAndHandles1.h[3][0]},${pointsAndHandles1.h[3][1]}
${pointsAndHandles1.p[3][0]},${pointsAndHandles1.p[3][1]} `;
// 1st arc
metaball+=`
A ${pointsAndHandles1.r},${pointsAndHandles1.r}
0 1,1
${pointsAndHandles2.p[1][0]},${pointsAndHandles2.p[1][1]} `;
// 2ndt Bezier Curve
metaball+=`
C ${pointsAndHandles2.h[1][0]},${pointsAndHandles2.h[1][1]}
${pointsAndHandles2.h[3][0]},${pointsAndHandles2.h[3][1]}
${pointsAndHandles2.p[3][0]},${pointsAndHandles2.p[3][1]} `;
// 2nd arc
metaball+=`
A ${pointsAndHandles2.r},${pointsAndHandles2.r}
0 1,1
${pointsAndHandles1.p[1][0]},${pointsAndHandles1.p[1][1]} `;
metaball=metaball.replace(/\n/g,'').replace(/\s\s/g,'');
return metaball;
}
else
{
// Add "c" property (center) to all data elements
data.forEach(d=>d.c=[d.x,d.y]);
// Init subsetData
let subsetData = data;
// Can't do convex hull with less than 3 points
if (data.length>2)
{
// Calculate convex hull
const convex_hull = d3.polygonHull(data.map(d=>([d.x,d.y])));
// Subset the data according to convex hull
subsetData = convex_hull.reverse().map(d=>{
const elm=data.find(dd=>dd.x===d[0]&&dd.y===d[1]);
return elm;
});
}
// Save segments
metaball=metaballSegments(subsetData);
intersections.selectAll('*').remove();
// Do a second iteration:
// check for intersections with circles not part of subsetData
const check_intersections = data.filter(d=>subsetData.indexOf(d)<0);

for (let i=0; i<check_intersections.length; i++){
// Look for intersections
const this_circle = check_intersections[i];

var pathToSample = document.createElementNS('http://www.w3.org/2000/svg','path');
pathToSample.setAttribute('d',metaball);

const length = pathToSample.getTotalLength();
const rate = sampleRate;
const unit = length/rate;
const sampledPoints = [];
for (let j=0; j<rate; j++) {
sampledPoints.push(pathToSample.getPointAtLength(unit*j))
}

// const polygon = ShapeInfo.polygon(sampledPoints);
const path = ShapeInfo.path(metaball); // looks like it has a bug
const circle = ShapeInfo.circle({center: {x:this_circle.x, y:this_circle.y}, radius:this_circle.r});
const intersections_data = Intersection.intersect(path, circle);
console.log(intersections_data)
intersections_data.points.forEach(d=>{
intersections.append('circle').attr('r',2).attr('cx',d.x).attr('cy',d.y)
})
// d3.select
// If true
if (intersections_data.status==="Intersection") {
// Identify the two adjacent circles
const adjacent_circles = intersections_data.points.map(d=>{
const subset_with_distances = subsetData.map( (dd,i)=>{
const a = d.x - dd.x;
const b = d.y - dd.y;
dd.distance = Math.sqrt( a*a + b*b );
dd.position=i;
return dd;
}).sort((a,b)=>a.distance-b.distance);

return subset_with_distances[0];
})
// Insert the new circle between its two adjacent mates
if ( (adjacent_circles[0].position===0&&adjacent_circles[1].position===subsetData.length-1) || (adjacent_circles[1].position===0&&adjacent_circles[0].position===subsetData.length-1) ) {
subsetData.push(this_circle);
}
else
{
const append_after = adjacent_circles.sort((a,b)=>a.position-b.position)[0].position;
subsetData.splice(append_after+1, 0, this_circle);
}
//metaball=metaballSegments(subsetData);
}
}
}
return metaball;
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more