Public
Edited
Mar 30, 2024
Insert cell
Insert cell
sk.csv
Type Table, then Shift-Enter. Ctrl-space for more options.

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
const marginTop = 60;

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

// horizontal (x) encoding
const x = d3
.scaleLinear()
.domain(d3.extent(sk, (d) => d["total_weeks"]))
.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", "white")
.attr("style", "max-width: 100%; height: auto;");

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

svg
.append("text")
.attr("x", width / 2)
.attr("y", marginTop / 1.5) // Positioning the title halfway through the top margin
.attr("text-anchor", "middle") // This will center the text at the specified (x, y)
.style("font-size", "45px")
.style("font-family", "Nosifer") // Adjust font size as needed
.style("fill", "#c81d25") // Adjust text color as needed
.text("The Master of Macabre");

// Append subtitle
svg
.append("text")
.attr("x", width / 2)
.attr("y", (marginTop / 4) * 5.3) // Positioning the subtitle a bit lower than the title
.attr("text-anchor", "middle") // This will center the text at the specified (x, y)
.style("font-size", "25px") // Adjust font size as needed
.style("fill", "#c81d25") // Adjust text color as needed
.style("font-family", "Josefin Sans")
.text("Stephen King’s Weeks on the New York Times Bestseller List");

// 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", "black");

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

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

// Append x-axis title
xAxis
.append("text")
.attr("class", "x-axis-label")
.attr("text-anchor", "end") // To align the text at the end of the axis
.attr("x", width - marginRight) // Position at the end of the x-axis
.attr("y", -10) // Position above the axis line, adjust as needed
.style("fill", "black") // Text color
.style("font-family", "Josefin Sans")
.style("font-weight", "bold")
.text("Number of Weeks on Bestseller List ➡");

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

// 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", "#c81d25")
.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", "#c81d25")
.style("opacity", 0.8)
.style("border-radius", "5px")
.style("padding", "10px")
.style("color", "white")
.style("font-family", "Josefin Sans")
.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;

// Set the tooltip content with year above the title
tooltip
.html(`<strong>${d.data.year}</strong><br>${d.data.title}`) // Combining year and title
.style("visibility", "visible")
.style("left", `${x}px`)
.style("top", `${y}px`);

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
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
<style>

@import url('https://fonts.googleapis.com/css2?family=Nosifer&display=swap');
@import url('https://fonts.googleapis.com/css2?family=Josefin+Sans&display=swap');

</style>
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