|
|
<!DOCTYPE html> |
|
|
<html lang="en"> |
|
|
<head> |
|
|
<meta charset="UTF-8" /> |
|
|
<title>Unified Sankey Diagram</title> |
|
|
<script src="https://d3js.org/d3.v7.min.js"></script> |
|
|
<script src="https://cdn.jsdelivr.net/npm/d3-sankey@0.12.3/dist/d3-sankey.min.js"></script> |
|
|
<style> |
|
|
body { font-family: sans-serif; } |
|
|
svg { font: bold 14px sans-serif; } |
|
|
</style> |
|
|
</head> |
|
|
<body> |
|
|
<svg id="sankey"></svg> |
|
|
<script> |
|
|
const data = [ |
|
|
{ source: "Total", target: "Checkpoint", value: 4520 }, |
|
|
{ source: "Total", target: "Other", value: 1327 }, |
|
|
{ source: "Total", target: "Adapters", value: 34153 }, |
|
|
{ source: "Adapters", target: "Textual Training Data", value: 11151 }, |
|
|
{ source: "Adapters", target: "Unknown Textual Training Data", value: 23002 }, |
|
|
{ source: "Textual Training Data", target: "POI True", value: 2327 }, |
|
|
{ source: "Textual Training Data", target: "POI False", value: 8824 }, |
|
|
{ source: "POI True", target: "explicit", value: 238 }, |
|
|
{ source: "POI True", target: "non-explicit", value: 2089 }, |
|
|
{ source: "POI False", target: "explicit", value: 4732 }, |
|
|
{ source: "POI False", target: "non-explicit", value: 4092 }, |
|
|
{ source: "explicit", target: "loli", value: 558 }, |
|
|
{ source: "explicit", target: "shota", value: 69 }, |
|
|
{ source: "explicit", target: "rape", value: 189 } |
|
|
]; |
|
|
|
|
|
|
|
|
|
|
|
function inlineStyles(svgElement) { |
|
|
const styles = ` |
|
|
text { font-family: sans-serif; fill: black; font-weight: bold; } |
|
|
rect { stroke: none; } |
|
|
`; |
|
|
const styleElem = document.createElementNS("http://www.w3.org/2000/svg", "style"); |
|
|
styleElem.textContent = styles; |
|
|
svgElement.insertBefore(styleElem, svgElement.firstChild); |
|
|
} |
|
|
|
|
|
function saveSVG() { |
|
|
const svgElement = document.querySelector("svg"); |
|
|
inlineStyles(svgElement); |
|
|
const serializer = new XMLSerializer(); |
|
|
const source = serializer.serializeToString(svgElement); |
|
|
const svgBlob = new Blob([source], { type: "image/svg+xml;charset=utf-8" }); |
|
|
const svgUrl = URL.createObjectURL(svgBlob); |
|
|
const downloadLink = document.createElement("a"); |
|
|
downloadLink.href = svgUrl; |
|
|
downloadLink.download = "sankey-diagram.svg"; |
|
|
document.body.appendChild(downloadLink); |
|
|
downloadLink.click(); |
|
|
document.body.removeChild(downloadLink); |
|
|
} |
|
|
|
|
|
window.addEventListener("keydown", function (e) { |
|
|
if (e.code === "Space") { |
|
|
e.preventDefault(); |
|
|
saveSVG(); |
|
|
} |
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const width = 680; |
|
|
const height = 400; |
|
|
|
|
|
const svg = d3.select("#sankey") |
|
|
.attr("width", width) |
|
|
.attr("height", height); |
|
|
|
|
|
const sankey = d3.sankey() |
|
|
.nodeId(d => d.name) |
|
|
.nodeAlign(d3.sankeyLeft) |
|
|
.nodeWidth(15) |
|
|
.nodePadding(20) |
|
|
.extent([[1, 1], [width - 1, height - 6]]); |
|
|
|
|
|
const nodeSet = new Set(); |
|
|
data.forEach(d => { |
|
|
nodeSet.add(d.source); |
|
|
nodeSet.add(d.target); |
|
|
}); |
|
|
const nodes = Array.from(nodeSet).map(name => ({ name })); |
|
|
|
|
|
const { nodes: layoutNodes, links: layoutLinks } = sankey({ |
|
|
nodes: nodes.map(d => Object.assign({}, d)), |
|
|
links: data.map(d => Object.assign({}, d)) |
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
const color = name => { |
|
|
const map = { |
|
|
"Total": "#C0C0C0", |
|
|
"Adapters": "#C0C0C0", |
|
|
"Checkpoint": "#C0C0C0", |
|
|
"Other": "#C0C0C0", |
|
|
"Textual Training Data": "#BC8F8F", |
|
|
"No Textual Training Data": "#ccc", |
|
|
"POI True": "#FFA07A", |
|
|
"POI False": "#BC8F8F", |
|
|
"explicit": "#DC143C", |
|
|
"non-explicit": "#C0C0C0", |
|
|
"loli": "#8A2BE2", |
|
|
"shota": "#8A2BE2", |
|
|
"rape": "#8A2BE2" |
|
|
}; |
|
|
return map[name] || "#ccc"; |
|
|
}; |
|
|
|
|
|
const labelMap = { |
|
|
"Textual Training Data": "Textual training data found", |
|
|
"No Textual Training Data": "No textual training data", |
|
|
"POI True": "POI true", |
|
|
"POI False": "POI false", |
|
|
"non-explicit": "Non-explicit", |
|
|
"explicit": "Explicit", |
|
|
"Total": "Total", |
|
|
"Adapters": "Adapters", |
|
|
"Checkpoint": "Checkpoint", |
|
|
"Other": "Other", |
|
|
"loli": "Loli", |
|
|
"shota": "Shota", |
|
|
"rape": "Rape" |
|
|
}; |
|
|
|
|
|
|
|
|
const defs = svg.append("defs"); |
|
|
layoutLinks.forEach((d, i) => { |
|
|
const grad = defs.append("linearGradient") |
|
|
.attr("id", d.uid = `link-${i}`) |
|
|
.attr("gradientUnits", "userSpaceOnUse") |
|
|
.attr("x1", d.source.x1) |
|
|
.attr("x2", d.target.x0); |
|
|
|
|
|
grad.append("stop").attr("offset", "0%").attr("stop-color", color(d.source.name)); |
|
|
grad.append("stop").attr("offset", "100%").attr("stop-color", color(d.target.name)); |
|
|
}); |
|
|
|
|
|
svg.append("g") |
|
|
.attr("fill", "none") |
|
|
.attr("stroke-opacity", 0.5) |
|
|
.selectAll("path") |
|
|
.data(layoutLinks) |
|
|
.join("path") |
|
|
.attr("d", d3.sankeyLinkHorizontal()) |
|
|
.attr("stroke", d => `url(#${d.uid})`) |
|
|
.attr("stroke-width", d => Math.max(1, d.width)) |
|
|
.append("title") |
|
|
.text(d => `${d.source.name} → ${d.target.name}\n${d.value}`); |
|
|
|
|
|
svg.append("g") |
|
|
.selectAll("rect") |
|
|
.data(layoutNodes) |
|
|
.join("rect") |
|
|
.attr("x", d => d.x0) |
|
|
.attr("y", d => d.y0) |
|
|
.attr("height", d => d.y1 - d.y0) |
|
|
.attr("width", d => d.x1 - d.x0) |
|
|
.attr("fill", d => color(d.name)) |
|
|
.append("title") |
|
|
.text(d => `${d.name}\n${d.value}`); |
|
|
|
|
|
svg.append("g") |
|
|
.selectAll("text") |
|
|
.data(layoutNodes) |
|
|
.join("text") |
|
|
.attr("x", d => d.x0 < width / 2 ? d.x1 + 6 : d.x0 - 6) |
|
|
.attr("y", d => (d.y1 + d.y0) / 2) |
|
|
.attr("dy", "0.35em") |
|
|
.attr("text-anchor", d => d.x0 < width / 2 ? "start" : "end") |
|
|
.style("font-size", "16px") |
|
|
.each(function(d) { |
|
|
const text = d3.select(this); |
|
|
const label = labelMap[d.name] || d.name; |
|
|
const lines = label.split(/(?<=\w)\s+(?=\w)/); |
|
|
|
|
|
lines.forEach((line, i) => { |
|
|
text.append("tspan") |
|
|
.attr("x", d.x0 < width / 2 ? d.x1 + 6 : d.x0 - 6) |
|
|
.attr("dy", i === 0 ? "0.35em" : "1.2em") |
|
|
.text(line); |
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const format = d3.format(","); |
|
|
text.append("tspan") |
|
|
.attr("x", d.x0 < width / 2 ? d.x1 + 6 : d.x0 - 6) |
|
|
.attr("dy", "1.5em") |
|
|
.style("font-size", "14px") |
|
|
.style("fill", "#333") |
|
|
.text(`${format(d.value)}`); |
|
|
|
|
|
|
|
|
}); |
|
|
</script> |
|
|
</body> |
|
|
</html> |
|
|
|