Public
Edited
Nov 26, 2023
Insert cell
Insert cell
Insert cell
Insert cell
chart_2 = {
// chart dimensions and margins
const width = 1000;
const height = 600;
const marginRight = 20;
const marginLeft = 20;
const marginBottom = 20;
const popScale = 2; // Scale factor for pop effect

// Dot size and padding
const radius = 16;
const padding = 7;

// horizontal (x) encoding
const x = d3
.scaleLinear()
.domain(d3.extent(alone, (d) => d["days_lasted"]))
.range([marginLeft, width - marginRight]);

// Create SVG container
const svg = d3
.create("svg")
.attr("width", width)
.attr("height", height)
.attr("viewBox", [0, 0, width, height])
.style("background-color", "black")
.attr("style", "max-width: 100%; height: auto;");

svg
.append("rect")
.attr("width", width)
.attr("height", height)
.attr("fill", "black");

// Add the x axis with modified color
const xAxis = svg
.append("g")
.attr("transform", `translate(0,${height - marginBottom})`)
.call(d3.axisBottom(x).tickSizeOuter(0));

// Change the color of the x-axis line to white
xAxis.select(".domain").attr("stroke", "white");

// Change the color of the ticks to white
xAxis.selectAll(".tick line").attr("stroke", "white");

// Change the color of the tick labels (text) to white
xAxis
.selectAll(".tick text")
.attr("fill", "white")
.style("font-family", "Ubuntu") // Set the font family
.style("font-size", "12px") // Set the font size
.style("font-weight", "bold");

// Processed data from dodge function
const processedData = dodge(alone, {
radius: radius * 2 + padding,
x: (d) => x(d["days_lasted"])
});

// Create group elements for each data point
const groups = svg
.selectAll("g.data-point")
.data(processedData)
.enter()
.append("g")
.attr("class", "data-point")
.on("mouseover", handleMouseOver)
.on("mouseout", handleMouseOut);

// Append circles inside each group
groups
.append("circle")
.attr("cx", (d) => d.x)
.attr("cy", (d) => height / 2 - radius - padding - d.y)
.attr("r", radius)
.attr("stroke", (d) => {
if (d.data.WinnerLabel === "Winner") {
return "gold"; // Gold for winners
} else {
return d.data.gender === "Male" ? "lightblue" : "lightpink"; // Lightblue for male, lightpink for female
}
})
.attr("stroke-width", 3);

// Append images inside each group
groups
.append("image")
.attr("xlink:href", (d) => d.data.image_url)
.attr("x", (d) => d.x - radius)
.attr("y", (d) => height / 2 - radius - padding - d.y - radius)
.attr("width", radius * 2)
.attr("height", radius * 2)
.attr(
"clip-path",
"circle(" + radius + "px at " + radius + "px " + radius + "px)"
)
.attr("preserveAspectRatio", "xMidYMid slice");

const tooltip = d3
.select("body")
.append("div")
.attr("class", "tooltip")
.style("background", "lightsteelblue")
.style("border-radius", "5px")
.style("padding", "10px")
.style("color", "black")
.style("position", "absolute")
.style("text-align", "center")
.style("visibility", "hidden");

// Function to handle mouseover event
function handleMouseOver(event, d) {
const x = event.pageX;
const y = event.pageY;
const element = d3.select(this);
// Calculate the center of the circle
const centerX = d.x;
const centerY = height / 2 - radius - padding - d.y;

tooltip
.html(d.data.name) // Use the 'name' property from your data
.style("visibility", "visible")
.style("left", `${x}px`)
.style("top", `${y}px`)
.style(
"background-color",
d.data.gender === "Male" ? "lightblue" : "lightpink"
);

element
.selectAll("circle, image")
.raise() // Bring the element to the top in the SVG stacking context
.transition()
.duration(200)
// Scale around the center of the circle
.attr(
"transform",
`translate(${centerX},${centerY}) scale(${popScale}) translate(${-centerX},${-centerY})`
);
}

// Function to handle mouseout event
function handleMouseOut(event, d) {
const element = d3.select(this);
// Calculate the center of the circle
const centerX = d.x;
const centerY = height / 2 - radius - padding - d.y;

tooltip.style("visibility", "hidden");

element
.selectAll("circle, image")
.transition()
.duration(200)
// Scale back to normal around the center of the circle
.attr(
"transform",
`translate(${centerX},${centerY}) scale(1) translate(${-centerX},${-centerY})`
);
}
// Optionally, create a clipping path for circular images
svg
.append("defs")
.append("clipPath")
.attr("id", "circle-clip")
.append("circle")
.attr("cx", radius)
.attr("cy", radius)
.attr("r", radius);

return svg.node();
}
Insert cell
Insert cell
Insert cell
dodge = (data, { radius, x }) => {
const radius2 = radius ** 2;
const circles = data
.map((d) => ({ x: x(d), data: d }))
.sort((a, b) => a.x - b.x);
const epsilon = 1e-3;
let head = null,
tail = null;

// Returns true if circle ⟨x,y⟩ intersects with any circle in the queue.
function intersects(x, y) {
let a = head;
while (a) {
if (radius2 - epsilon > (a.x - x) ** 2 + (a.y - y) ** 2) {
return true;
}
a = a.next;
}
return false;
}

// Place each circle sequentially.
for (const b of circles) {
// Remove circles from the queue that can’t intersect the new circle b.
while (head && head.x < b.x - radius2) head = head.next;

// Choose the minimum non-intersecting tangent.
if (intersects(b.x, (b.y = 0))) {
let a = head;
b.y = Infinity;
do {
let y1 = a.y + Math.sqrt(radius2 - (a.x - b.x) ** 2);
let y2 = a.y - Math.sqrt(radius2 - (a.x - b.x) ** 2);
if (Math.abs(y1) < Math.abs(b.y) && !intersects(b.x, y1)) b.y = y1;
if (Math.abs(y2) < Math.abs(b.y) && !intersects(b.x, y2)) b.y = y2;
a = a.next;
} while (a);
}

// Add b to the queue.
b.next = null;
if (head === null) head = tail = b;
else tail = tail.next = b;
}

return circles;
}
Insert cell
Insert cell

One platform to build and deploy the best data apps

Experiment and prototype by building visualizations in live JavaScript notebooks. Collaborate with your team and decide which concepts to build out.
Use Observable Framework to build data apps locally. Use data loaders to build in any language or library, including Python, SQL, and R.
Seamlessly deploy to Observable. Test before you ship, use automatic deploy-on-commit, and ensure your projects are always up-to-date.
Learn more