(async () => {
const data = await FileAttachment("keeping-track-of-the-big-picture5.csv").csv({ typed: true });
const getIcon = (itemText) => {
return "M 0 -5 L 5 0 L 0 5 L -5 0 Z";
};
const splitText = (text) => {
const parts = text.split(" ");
const middleIndex = Math.ceil(parts.length / 2);
return [parts.slice(0, middleIndex).join(" "), parts.slice(middleIndex).join(" ")];
};
const getRandomBottomPosition = (radius, topPercentage = 0.4) => {
const centerBottomY = radius * 0.6;
const verticalSpread = radius * 0.2;
const topBoundary = -radius * (1 - topPercentage);
const minRadius = 0;
const maxRadius = radius * 0.8;
let x, y;
do {
const r = Math.random() * (maxRadius - minRadius) + minRadius;
const angle = Math.random() * 2 * Math.PI;
x = r * Math.cos(angle);
y = centerBottomY + (Math.random() * 2 - 1) * verticalSpread;
} while (y < topBoundary || Math.sqrt(x * x + y * y) > radius * 0.8);
return { x, y };
};
// 3. Chart definition
const chart = () => {
// Color mapping for the outer circles based on "Trend"
const trendColor = d3.scaleOrdinal()
.domain([
"Growing self-censorship",
"Harm to independent outlets",
"Erosion of public access",
"Media curry favor with the President",
"Hostility of media environment"
])
.range([
"#730362",
"#9D0998",
"#0CA5B3",
"#1A2E7F",
"#28124A"
]);
// Color mapping for the inner icons based on "Type"
const typeColor = (type) => {
if (type === "good") return "#1CD81F";
if (type === "bad") return "#FF312E";
return "#ccc"; // Default color if type is not "good" or "bad"
};
const root = d3.hierarchy({
name: "root",
children: Array.from(d3.group(data, d => d.Trend), ([name, children]) => ({
name: name,
children: children.map(d => ({ ...d, item: d.Items, value: 1 })), // Store d.Items as "item"
}))
}).sum(d => d.value);
const packed = d3.pack()
.size([width, height])
.padding(10)(root);
const svg = d3.select(DOM.svg(width, height))
.style("width", "100%")
.style("height", "auto")
.style("font", "10px sans-serif")
.style("overflow", "visible");
const node = svg.append("g")
.selectAll("g")
.data(packed.descendants().slice(1))
.join("g")
.attr("transform", d => `translate(${d.x},${d.y})`);
node.append("circle")
.attr("r", d => d.r)
.style("fill", d => d.depth === 1 ? trendColor(d.data.name) : "none") // Color outer circles by trend
.style("stroke", "none");
const labelGroup = node.filter(d => d.depth === 1)
.append("g")
.attr("transform", d => `translate(0, ${-d.r * 0.6})`); // Move labels towards the top
labelGroup.selectAll("text")
.data(d => splitText(d.data.name))
.join("text")
.attr("x", 0)
.attr("y", (d, i) => i * 1.1 - 0.5 + "em")
.attr("text-anchor", "middle")
.style("fill", "white")
.style("font-family", "Roboto")
.style("font-size", "1em")
.style("dominant-baseline", "middle")
.text(d => d);
const leaf = node.filter(d => d.depth === 2)
.append("g") // Append a group for each leaf node
.attr("transform", d => {
const { x, y } = getRandomBottomPosition(d.r, 0.4);
const verticalOffset = d.r * 0.2; // Adjust this value to move the group down
return `translate(${x}, ${y + verticalOffset})`;
});
leaf.append("path")
.attr("d", getIcon)
.style("fill", d => typeColor(d.data.Type))
.style("stroke", "white")
.style("stroke-width", 0.35)
.attr("transform", d => `scale(${(d.r / 12) * 0.7})`); // Apply scale relative to the group's origin
leaf.append("title")
.text(d => d.data.item);
return svg.node();
};
const width = 700; // Increased width for better spacing
const height = 550; // Increased height for better spacing
return chart();
})()