chart = {
const reviewRanges = [
{ min: 0, max: 1300, symbol: d3.symbolCircle },
{ min: 1301, max: 1500, symbol: d3.symbolSquare },
{ min: 1501, max: 1700, symbol: d3.symbolTriangle },
{ min: 1701, max: 1900, symbol: d3.symbolCross },
{ min: 1901, max: 2000, symbol: d3.symbolDiamond },
{ min: 2001, max: Infinity, symbol: d3.symbolStar }
];
const getSymbol = reviews => {
const range = reviewRanges.find(r => reviews >= r.min && reviews <= r.max);
return range ? range.symbol : d3.symbolCircle;
};
function cleanData(node) {
if (!node || typeof node !== "object") return null;
node.name = node.name || "Unnamed";
if (node.children) {
const establishments = [];
node.children.forEach(ratingNode => {
if (ratingNode.children) {
ratingNode.children.forEach(child => {
if (child && typeof child === "object") {
child.name = child.name || "Unnamed";
child.value = Number.isFinite(child.value) ? child.value : 1;
child.reviews = Number.isFinite(child.value) ? Math.round(child.value * 400) : 0;
child.group = child.group || "default";
delete child.children;
establishments.push(child);
}
});
}
});
node.children = establishments.filter(child => child !== null);
if (node.children.length === 0) delete node.children;
} else {
delete node.children;
}
node.reviews = Number.isFinite(node.reviews) ? Math.max(0, node.reviews) : 0;
node.value = Number.isFinite(node.value) ? node.value : 1;
node.group = node.group || "default";
return node;
}
// Clean the input data
const cleanedData = cleanData(JSON.parse(JSON.stringify(data))); // Deep copy
if (!cleanedData || !cleanedData.name) {
throw new Error("Invalid data: Root node must have a name and be an object");
}
// Create hierarchy
const root = tree(d3.hierarchy(cleanedData)
.sort((a, b) => d3.descending(a.data.value, b.data.value)) // Sort by rating
// .sort((a, b) => a.data.name.toLowerCase().localeCompare(b.data.name.toLowerCase())) // Alphabetical
// .sort((a, b) => d3.descending(a.data.reviews, b.data.reviews)) // By reviews
);
// Debugging
console.log("Cleaned data:", cleanedData);
console.log("Hierarchy root:", root);
root.descendants().forEach(d => {
if (!d.data) console.warn("Node missing data:", d);
});
setColor(root);
const svg = d3.create("svg");
// Draw links between nodes
svg.append("g")
.attr("fill", "none")
.attr("stroke", "grey")
.attr("stroke-opacity", 0.4)
.attr("stroke-width", 1.5)
.selectAll("path")
.data(root.links())
.join("path")
.attr("d", d3.linkRadial()
.angle(d => d.x)
.radius(d => d.y))
.each(function(d) { d.target.linkNode = this; })
.attr("stroke", d => d.target.data ? color(d.target.data.name) : "grey");
// Draw nodes as symbols based on reviews
svg.append("g")
.selectAll("path")
.data(root.descendants())
.join("path")
.attr("transform", d => `
rotate(${d.x * 180 / Math.PI - 90})
translate(${d.y},0)
`)
.attr("fill", d => d.data ? color(d.data.name) : "#000")
.attr("d", d => d.data ? d3.symbol()
.type(d.children ? d3.symbolCircle : getSymbol(d.data.reviews || 0))
.size(d => d.children ? 50 : CirSize(d.data.value) * 10)() : "")
.append("title")
.text(d => d.data ? `Name: ${d.data.name}\nRating: ${d.data.value || 0}\nReviews: ${d.data.reviews || 0}` : "Invalid node");
// Add text labels
svg.append("g")
.attr("font-family", "sans-serif")
.attr("font-size", 12)
.attr("stroke-linejoin", "round")
.attr("stroke-width", 3)
.selectAll("text")
.data(root.descendants())
.join("text")
.attr("transform", d => `
rotate(${d.x * 180 / Math.PI - 90})
translate(${d.y},0)
rotate(${d.x >= Math.PI ? 180 : 0})
`)
.attr("dy", "0.31em")
.attr("x", d => d.x < Math.PI === !d.children ? 6 : -6)
.attr("text-anchor", d => d.x < Math.PI === !d.children ? "start" : "end")
.text(d => d.data ? d.data.name : "Unnamed")
.clone(true).lower()
.attr("stroke", "white");
// Add legend for review ranges
const legend = svg.append("g")
.attr("transform", "translate(-100, -100)");
reviewRanges.forEach((range, i) => {
legend.append("path")
.attr("d", d3.symbol().type(range.symbol).size(50)())
.attr("transform", `translate(0, ${i * 20})`)
.attr("fill", "black");
legend.append("text")
.attr("x", 10)
.attr("y", i * 20 + 5)
.attr("font-family", "sans-serif")
.attr("font-size", 10)
.text(`${range.min}–${range.max} reviews`);
});
return svg.attr("viewBox", autoBox).node();
}