function Sunburst(data, {
width = 700,
height = 700,
fontCenter = 32,
fontTask = 15,
fontDataset = 13,
fontPercent = 12,
innerPad = 0,
outerPad = 0,
percentRingW = 60,
startAngle = 0,
endAngle = 2 * Math.PI,
colorScheme = d3.interpolateRainbow,
value = d => d.value ?? 0
} = {}) {
const radius = Math.min(width, height) / 2 - outerPad - percentRingW;
const children = d => d.children?.filter(c => !c.is_percentage_ring);
const root = d3.hierarchy(data, children).sum(d => Math.max(0, value(d)));
d3.partition().size([endAngle - startAngle, radius])(root);
const hues = d3.quantize(colorScheme, 16);
root.children.forEach((t, i) => {
const base = d3.color(hues[i]);
const white = d3.color("#ffffff");
t.hue = d3.interpolateRgb(base, white)(0.3);
});
const taskHue = d => {
if (d.depth === 0) return "#ddd";
if (d.depth === 1) return d.hue;
const task = d.ancestors().find(a => a.depth === 1);
return task?.hue ?? "#999";
};
const arc = d3.arc()
.startAngle(d => d.x0 + startAngle)
.endAngle( d => d.x1 + startAngle)
.innerRadius(d => d.y0 + innerPad)
.outerRadius(d => d.y1 - innerPad);
const svg = d3.create("svg")
.attr("viewBox", [-width/2, -height/2, width, height])
.attr("width", width)
.attr("height", height)
.attr("style", "max-width:100%;height:auto;display:block;margin:auto;")
.attr("text-anchor", "middle");
svg.append("g")
.selectAll("path")
.data(root.descendants())
.join("path")
.attr("d", arc)
.attr("fill", taskHue)
.attr("stroke", "#fff")
.attr("stroke-width", 0.4);
svg.append("text")
.attr("font-family", "Playfair Display, Georgia, serif")
.attr("font-size", fontCenter)
.attr("text-anchor", "middle")
.attr("y", -fontCenter * 0.3)
.selectAll("tspan")
.data(["AVA", "Bench"])
.join("tspan")
.attr("x", 0)
.attr("dy", (d, i) => i === 0 ? "0em" : "1.2em")
.text(d => d);
const LONG_TASKS = new Set(["Action Recognition", "Object Recognition", "Scene Recognition"]);
const LONG_DATA = new Set(["Crowd Surveillance Dataset"]);
svg.append("g")
.selectAll("text")
.data(root.descendants().filter(d => d.depth > 0 && (d.y1 - d.y0) * (d.x1 - d.x0) > 6))
.join("text")
.attr("transform", d => {
const a = ((d.x0 + d.x1)/2 + startAngle) * 180/Math.PI;
const r = (d.y0 + d.y1)/2;
return `rotate(${a-90}) translate(${r},0) rotate(${a<180?0:180})`;
})
.attr("font-family", "sans-serif")
.attr("font-size", d => {
if (d.depth === 1) {
const base = d.data.name.replace(/\s*\(.*/, "");
return base === "Action Recognition" ? fontTask - 4 : fontTask;
}
return fontDataset;
})
.attr("fill", "#000")
.selectAll("tspan")
.data(d => {
if (d.depth === 1) {
const base = d.data.name.replace(/\s*\(.*/, "");
return LONG_TASKS.has(base) ? base.split(" ") : [base];
}
if (d.depth > 1) {
const name = d.data.name;
if (LONG_DATA.has(name)) return ["Crowd Surveillance", "Dataset"];
return [name];
}
return [];
})
.join("tspan")
.attr("x", 0)
.attr("dy", (d,i)=> i ? "1em" : 0)
.text(d => d);
const percentArc = d3.arc()
.startAngle(d => d.x0 + startAngle)
.endAngle( d => d.x1 + startAngle)
.innerRadius(radius + outerPad)
.outerRadius(radius + outerPad + percentRingW);
const band = svg.append("g").selectAll("g").data(root.children).join("g");
band.append("path")
.attr("d", percentArc)
.attr("fill", d => d.hue)
.attr("fill-opacity", 0.18)
.attr("stroke", d => d.hue)
.attr("stroke-width", 0.4);
band.append("text")
.attr("transform", d => {
const a = ((d.x0 + d.x1)/2 + startAngle) * 180/Math.PI;
const r = radius + outerPad + percentRingW/2;
return `rotate(${a-90}) translate(${r},0) rotate(${a<180?0:180})`;
})
.attr("font-family", "sans-serif")
.attr("font-size", fontPercent)
.attr("font-weight", "bold")
.attr("fill", d => "#000")
.attr("text-anchor", "middle")
.selectAll("tspan")
.data(d => {
const rawTotal = d.data.name.match(/\((\d+)\)$/)?.[1];
const total = rawTotal ? d3.format(".1f")(+rawTotal / 1000) + "K" : "";
const pct = d.data.children.find(c => c.is_percentage_ring)?.name ?? "";
return [`${total}`, pct];
})
.join("tspan")
.attr("x", 0)
.attr("dy", (d,i)=> i ? "1em" : 0)
.text(d => d);
return svg.node();
}