(async () => {
const data = await FileAttachment("keeping-track-of-the-big-picture6.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 chart = () => {
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"
]);
const typeColor = (type) => {
if (type === "good") return "#1CD81F";
if (type === "bad") return "#FF312E";
return "#ccc";
};
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, url: d.URL })),
}))
}).sum(d => d.value);
const packed = d3.pack()
.size([width * 1.4, height * 1.4])
.padding(180)(root);
const svg = d3.select(DOM.svg(width, height))
.style("width", "100%")
.style("height", "auto")
.style("font", "10px sans-serif")
.style("overflow", "visible");
const zoom = d3.zoom()
.scaleExtent([0.5, 4])
.on("zoom", zoomed);
const transformGroup = svg.append("g");
transformGroup.selectAll(".trend-label").remove();
const node = transformGroup.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")
.style("stroke", "none");
node.filter(d => d.depth === 1)
.each(function (d) {
const circle = d3.select(this).select("circle");
const cx = d.x;
const cy = d.y;
const r = circle.attr("r");
let textX, textY, textAnchor;
let text = d.data.name;
if (text === "Growing self-censorship") {
text = "Growing\nself-censorship";
} else if (text === "Hostility of media environment") {
text = "Hostility of\nmedia environment";
} else if (text === "Media curry favor with the president") {
text = "Media curry favor\nwith the president";
} else if (text === "Erosion of public access") {
text = "Erosion of\npublic access";
} else if (text === "Harm to independent outlets") {
text = "Harm to\nindependent outlets";
}
const lines = text.split("\n");
const numLines = lines.length;
textX = cx;
textY = cy - r - 20;
textAnchor = "middle";
transformGroup.append("text")
.attr("class", "trend-label")
.attr("x", textX)
.attr("y", textY)
.attr("dy", numLines === 1 ? "0em" : `-${(numLines - 1) * 0.75}em`)
.attr("text-anchor", textAnchor)
.style("fill", trendColor(d.data.name))
.style("font-family", "Roboto")
.style("font-size", "1em")
.style("font-weight", "bold")
.each(function () {
const textElement = d3.select(this);
lines.forEach((line, index) => {
textElement.append("tspan")
.attr("x", textX)
.attr("y", textY)
.attr("dy", index === 0 ? "0em" : "1.2em")
.text(line);
});
});
});
const leaf = node.filter(d => d.depth === 2)
.style("cursor", "pointer")
.on("click", (event, d) => {
if (d.data.url) {
window.open(d.data.url, "_blank");
}
});
leaf.append("path")
.attr("d", getIcon)
.style("fill", d => typeColor(d.data.Type))
.style("stroke", "white")
.style("stroke-width", 0.35)
.attr("transform", d => {
const scaleFactor = (d.r / 12) * 2.4;
return `translate(0, 0) scale(${scaleFactor})`;
});
leaf.append("title")
.text(d => d.data.item);
svg.call(zoom);
function zoomed(event) {
transformGroup.attr("transform", event.transform);
}
return svg.node();
};
const width = 700;
const height = 550;
return chart();
})()