Public
Edited
Apr 29, 2023
1 star
Insert cell
Insert cell
data = FileAttachment("data.json").json()
Insert cell
radiusScale = d3
.scaleSqrt()
.domain(d3.extent(data, (d) => d.daly))
.range([3, 50])
Insert cell
## Notes

- Each quadtree node, contains exactly one data point.
- The data point's center will be somewhere in the quadtree node - but not necessarily in the middle of the square.
- Only the nodes with no length - ie the ones that are not an array and are an object containing a data key actually contain a data point, the other ones are just a result of diving the area into quads - they are empty quads.
Insert cell
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;
}
Insert cell
height = 500
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