canvas = {
const context = DOM.context2d(width, height);
const padding = 5;
context.font = "10px Arial";
context.textAlign = "center";
context.textBaseline = "middle";
const data_with_width_and_height = data.map((d) => {
const measureText = context.measureText(d.disease);
return {
...d,
width: measureText.width + padding,
height: parseInt(context.font, 10),
label_overlap: false
};
});
const simulation = d3
.forceSimulation(data_with_width_and_height)
.alphaTarget(0.5)
.velocityDecay(0.8)
.force("charge", d3.forceManyBody().strength(-width / 100))
.force(
"x",
d3
.forceX()
.strength(0.1)
.x(width / 2)
)
.force(
"y",
d3
.forceY()
.strength(0.1)
.y(height / 2)
)
.force(
"collide",
d3
.forceCollide()
.radius((d) => radiusScale(d.daly) + 3)
.iterations(50)
)
.on("tick", ticked);
function ticked() {
const overlapData = preventTextOverlap(simulation.nodes());
context.clearRect(0, 0, width, height);
for (const d of data_with_width_and_height) {
let radius = radiusScale(d.daly);
context.beginPath();
context.moveTo(d.x + radius, d.y);
context.arc(d.x, d.y, radius, 0, 2 * Math.PI);
context.fillStyle = "#ffff00";
context.fill();
context.beginPath();
context.moveTo(d.x + 1, d.y);
context.arc(d.x, d.y, 1, 0, 2 * Math.PI);
context.fillStyle = "#ff0000";
context.fill();
}
context.lineWidth = 1;
context.strokeStyle = "black";
overlapData.quads.forEach((d) => {
context.strokeRect(d.rectX, d.rectY, d.rectWidth, d.rectHeight);
});
overlapData.newData.forEach((d) => {
if (d.label_overlap) {
context.fillStyle = "black";
} else {
context.fillStyle = "red";
}
context.fillText(d.disease, d.x, d.y);
});
}
function preventTextOverlap(data) {
const quadtree = d3
.quadtree()
.extent([
[-1, -1],
[width + 1, height + 1]
])
.x((d) => d.x)
.y((d) => d.y)
.addAll(data);
let quads = [];
quadtree.visit((node, x1, y1, x2, y2) => {
// x1, y1⟩ are the lower bounds of the node, and ⟨x2, y2⟩ are the upper bounds
if (!node.length) {
let rectX = x1;
let rectY = y1;
let rectWidth = x2 - x1;
let rectHeight = y2 - y1;
quads.push({ rectX, rectY, rectWidth, rectHeight });
}
});
const newData = data.map((d, i) => {
let label_overlap = false;
let newLabelX = d.x;
let newLabelY = d.y;
let x = d.x - d.width / 2;
let y = d.y - d.height / 2;
let nx1 = x - padding; // left boundary
let nx2 = x + d.width + padding; // right boundery
let ny1 = y - padding; // top boundary
let ny2 = y + d.height + padding; // bottom boundery
quadtree.visit((node, x1, y1, x2, y2) => {
// x1, y1⟩ are the lower bounds of the node, and ⟨x2, y2⟩ are the upper bounds
if (node.data && node.data !== d) {
const overlapX = nx2 > x1 && nx1 < x2;
const overlapY = ny2 > y1 && ny1 < y2;
if (overlapX && overlapY) {
// Adjust the label position to prevent overlap
// Update d.x and d.y based on your specific requirements
label_overlap = true;
}
}
return x1 > nx2 || x2 < nx1 || y1 > ny2 || y2 < ny1;
});
return {
...d,
label_overlap
};
});
return { quads, newData };
}
invalidation.then(() => simulation.stop()); // a promise to stop the simulation when the cell is re-run
return context.canvas;
}