Created
July 14, 2023 21:56
-
-
Save promethyttrium/6480217833998c10c6290e464b12840c to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
let nodes = [{ | |
name: "0a" | |
}, | |
{ | |
name: "0b" | |
}, | |
{ | |
name: "0c" | |
}, | |
{ | |
name: "0d" | |
}, | |
{ | |
name: "1" | |
}, | |
{ | |
name: "2a" | |
}, | |
{ | |
name: "2", | |
top: true | |
}, | |
{ | |
name: "3" | |
}, | |
{ | |
name: "3a" | |
}, | |
{ | |
name: "4a" | |
}, | |
{ | |
name: "4b" | |
}, | |
{ | |
name: "4c" | |
}, | |
{ | |
name: "4d" | |
}, | |
{ | |
name: "5" | |
}, | |
{ | |
name: "5a" | |
}, | |
{ | |
name: "6a" | |
}, | |
{ | |
name: "6b" | |
}, | |
{ | |
name: "7" | |
}, | |
{ | |
name: "8" | |
}, | |
{ | |
name: "8a" | |
}, | |
{ | |
name: "9" | |
}, | |
] | |
let links = [{ | |
source: "0a", | |
target: "1", | |
value: 25 | |
}, { | |
source: "0b", | |
target: "1", | |
value: 83 | |
}, { | |
source: "0c", | |
target: "1", | |
value: 23 | |
}, { | |
source: "0d", | |
target: "1", | |
value: 2 | |
}, { | |
source: "1", | |
target: "2", | |
value: 100 | |
}, { | |
source: "1", | |
target: "2a", | |
value: 3 | |
}, { | |
source: "1", | |
target: "3", | |
value: 30 | |
}, { | |
source: "3", | |
target: "4a", | |
value: 2 | |
}, { | |
source: "3", | |
target: "4b", | |
value: 3 | |
}, { | |
source: "3", | |
target: "4c", | |
value: 5 | |
}, { | |
source: "3", | |
target: "4d", | |
value: 5 | |
}, { | |
source: "3", | |
target: "5", | |
value: 15 | |
}, { | |
source: "3a", | |
target: "5", | |
value: 2 | |
}, { | |
source: "5", | |
target: "6a", | |
value: 4 | |
}, { | |
source: "5", | |
target: "6b", | |
value: 3 | |
}, { | |
source: "5", | |
target: "7", | |
value: 10 | |
}, { | |
source: "5a", | |
target: "7", | |
value: 1 | |
}, { | |
source: "7", | |
target: "8", | |
value: 10 | |
}, { | |
source: "7", | |
target: "8a", | |
value: 1 | |
}, { | |
source: "8", | |
target: "9", | |
value: 10 | |
}] | |
let dx = 15; | |
function sortLinks(a, b) { | |
if (a.target.top) { | |
return -1; | |
} else if (b.target.top) { | |
return 1; | |
} else if ( | |
a.target.sourceLinks.length == 0 && | |
b.target.sourceLinks.length > 0 | |
) { | |
return 1; | |
} else if ( | |
b.target.sourceLinks.length == 0 && | |
a.target.sourceLinks.length > 0 | |
) { | |
return -1; | |
} else if ( | |
a.source.targetLinks.length == 0 && | |
b.source.targetLinks.length > 0 | |
) { | |
return -1; | |
} else if ( | |
b.source.targetLinks.length == 0 && | |
a.source.targetLinks.length > 0 | |
) { | |
return 1; | |
} else { | |
return b.value - a.value; | |
} | |
} | |
let sankeyLink = d3.sankeyLinkHorizontal() | |
let svgWidth = 1000; | |
let svgHeight = 600; | |
let chartPadding = 75; | |
let chartWidth = svgWidth - chartPadding - chartPadding; | |
let chartHeight = svgHeight - chartPadding - chartPadding; | |
let bkdColour = "#edf3f5"; | |
let mainColour = "#f05d7b"; | |
// rotate = false | |
// dx = 15 | |
// svgWidth = 1000 | |
// svgHeight = 600 | |
// chartPadding = 75 | |
// chartHeight = 450 | |
// chartWidth = 850 | |
const sankey = d3 | |
.sankey() | |
.nodeId(d => d.name) | |
.nodeAlign(d3.sankeyCenter) | |
.nodeWidth(dx) | |
.nodePadding(10) | |
.linkSort(sortLinks) | |
.extent([ | |
[1, 5], | |
[chartWidth - 1, chartHeight - 5] | |
]); | |
let throughlineStart = 2 | |
let bottomGapX = 2 | |
function updateNodeY(node, dy) { | |
node.y0 = node.y0 + dy; | |
node.y1 = node.y1 + dy; | |
node.sourceLinks.forEach(function (link) { | |
link.y0 = link.y0 + dy; | |
}); | |
node.targetLinks.forEach(function (link) { | |
link.y1 = link.y1 + dy; | |
}); | |
} | |
function recalculateNodeCoordinates(inputGraph) { | |
let graph = clone(inputGraph); | |
let throughLineY = 0; | |
let firstThroughLine = true; | |
let maxHeight = d3.max(graph.nodes, d => d.height); | |
let maxDepth = d3.max(graph.nodes, d => d.depth); | |
graph.nodes.forEach(function (node) { | |
if ( | |
((node.sourceLinks.length > 0 && node.targetLinks.length > 0) || | |
node.depth == maxDepth) && | |
node.depth >= throughlineStart | |
) { | |
node.throughLine = true; | |
if (firstThroughLine) { | |
firstThroughLine = false; | |
node.targetLinks.forEach(function (link) { | |
if (link.source.targetLinks.length > 0) { | |
updateNodeY(node, link.y0 - link.y1); | |
} | |
}); | |
} else { | |
node.targetLinks.forEach(function (link) { | |
if (link.source.throughLine) { | |
let diff = link.y0 - link.y1; | |
updateNodeY(node, link.y0 - link.y1); | |
} | |
}); | |
} | |
} | |
if (node.sourceLinks.length == 0 && node.depth != maxDepth) { | |
let columnWidth = node.x0 - node.targetLinks[0].source.x1; | |
let yOffset = 0; | |
if (node.top) { | |
node.exit = "top"; | |
yOffset = chartPadding + node.targetLinks[0].source.y0; | |
} else { | |
node.exit = "bottom"; | |
let multiplier = | |
node.targetLinks[0].source.y1 - | |
(node.targetLinks[0].y0 + node.targetLinks[0].width / 2); | |
let gap = multiplier * bottomGapX; | |
yOffset = | |
chartPadding + chartHeight - node.targetLinks[0].y0 - dx / 2 + gap; | |
} | |
node.x0 = node.x0 - columnWidth + yOffset; | |
node.x1 = node.x0 + (node.y1 - node.y0); | |
if (node.top) { | |
node.y0 = -chartPadding; | |
} else { | |
node.y0 = chartHeight + chartPadding - dx; | |
} | |
node.y1 = node.y0 + dx; | |
} | |
if (node.targetLinks.length == 0 && node.height < maxHeight) { | |
node.middleEntry = true; | |
let diff = node.sourceLinks[0].target.y0 - chartPadding - node.y0; | |
updateNodeY(node, diff); | |
node.x0 = node.sourceLinks[0].target.x0 - chartPadding; | |
node.x1 = node.x0 + (node.y1 - node.y0); | |
node.y1 = node.y0 + dx; | |
} | |
node.cx = (node.x1 + node.x0) / 2; | |
node.cy = (node.y1 + node.y0) / 2; | |
if (node.middleEntry || node.top) { | |
node.labelX = node.cx; | |
node.labelY = node.y1 - 3; | |
node.labelAlign = "start"; | |
} else if (node.depth == 0) { | |
node.labelX = node.cx; | |
node.labelY = node.cy; | |
node.labelAlign = "middle"; | |
} else if (node.exit == "bottom") { | |
node.labelX = node.cx; | |
node.labelY = node.y0 + 3; | |
node.labelAlign = "end"; | |
} else { | |
node.labelX = node.cx; | |
node.labelY = node.y0 - 3; | |
node.labelAlign = "start"; | |
} | |
}); | |
return graph; | |
} | |
let graph2 = | |
sankey({ | |
nodes: nodes.map(d => Object.assign({}, d)), | |
links: links.map(d => Object.assign({}, d)) | |
}); | |
let graph3 = recalculateNodeCoordinates(graph2) | |
// console.log(graph3) | |
function nodeHeight(node) { | |
if (node.exit == "top" || node.exit == "bottom") { | |
return dx; | |
} else { | |
return node.y1 - node.y0; | |
} | |
} | |
function nodeFill(node) { | |
if (node.exit == "top" || node.exit == "bottom" || node.depth == 0) { | |
return "none"; | |
} else { | |
return "url(#hash)"; | |
} | |
} | |
function linkStroke(link) { | |
if (link.source.middleEntry) { | |
return "url(#linearMiddle)"; | |
} else if (link.source.depth == 0) { | |
return "url(#linearStart)"; | |
} else if (link.target.exit == "bottom") { | |
return "url(#linearBottom)"; | |
} else if (link.target.exit == "top") { | |
return "none"; | |
} else { | |
return mainColour; | |
} | |
} | |
function linkFill(link) { | |
return link.target.exit == "top" ? mainColour : "none"; | |
} | |
function linkStrokeWidth(link) { | |
return link.target.exit == "top" ? 0 : link.width; | |
} | |
function nodeWidth(node) { | |
if (node.exit == "top" || node.exit == "bottom") { | |
return node.y1 - node.y0; | |
} else { | |
return dx; | |
} | |
} | |
function calculatePathData(inputGraph) { | |
let graph = clone(inputGraph); | |
graph.links.forEach(function (link) { | |
if (link.width >= largeValueThreshold) { | |
link.large = true; | |
link.target.large = true; | |
} | |
if (link.target.exit == "top") { | |
link.x1 = link.target.x0 + link.width / 2; | |
link.y1 = dx - chartPadding; | |
link.path = | |
"M " + | |
link.source.x1 + | |
" " + | |
link.source.y0 + | |
" " + | |
"Q " + | |
link.target.x0 + | |
" " + | |
link.source.y0 + | |
" " + | |
link.target.x0 + | |
" " + | |
link.y1 + | |
" " + | |
"L " + | |
(link.target.x0 + link.width) + | |
" " + | |
link.y1 + | |
" " + | |
"Q " + | |
(link.target.x0 + link.width) + | |
" " + | |
(link.y0 + link.width / 2) + | |
" " + | |
link.source.x1 + | |
" " + | |
(link.y0 + link.width / 2) + | |
" " + | |
link.source.x1 + | |
" " + | |
link.source.y1 + | |
" z"; | |
} else if (link.target.exit == "bottom") { | |
link.x1 = link.target.x0 + link.width / 2; | |
link.y1 = chartHeight + chartPadding - dx; | |
let xLength = link.x1 - link.source.x1; | |
let yLength = link.y1 - link.y0; | |
let xLineLength = xLength - yLength; | |
link.path = | |
"M " + | |
link.source.x1 + | |
" " + | |
link.y0 + | |
" " + | |
"L " + | |
(link.source.x1 + xLineLength) + | |
" " + | |
link.y0 + | |
" " + | |
"Q " + | |
link.x1 + | |
" " + | |
link.y0 + | |
" " + | |
link.x1 + | |
" " + | |
link.y1; | |
} else if (link.source.middleEntry) { | |
link.x0 = link.source.x0 + link.width / 2; | |
link.y0 = link.source.y1; | |
link.x1 = link.target.x0; | |
link.path = | |
"M " + | |
link.x0 + | |
" " + | |
link.y0 + | |
" " + | |
"Q " + | |
link.x0 + | |
" " + | |
link.y1 + | |
" " + | |
link.x1 + | |
" " + | |
link.y1; | |
} else { | |
link.path = sankeyLink(link); | |
} | |
}); | |
return graph; | |
} | |
let largeValueThreshold = 50 | |
let graph = calculatePathData(graph3) | |
let width = 1000; | |
let height = 600; | |
let rotate = false; | |
const svg = d3 | |
.select('body') | |
.append("svg") | |
.attr("width", svgWidth) | |
.attr("height", svgHeight); | |
let backgroundRect = svg | |
.append('rect') | |
.attr("x", 0) | |
.attr("y", 0) | |
.attr("width", svgWidth) | |
.attr("height", svgWidth) | |
.style("fill", bkdColour); | |
let defs = svg.append("defs"); | |
let hash = defs | |
.append("pattern") | |
.attr("id", "hash") | |
.attr("width", 6) | |
.attr("height", 8) | |
.attr("patternUnits", "userSpaceOnUse") | |
.attr("patternTransform", "rotate(135)"); | |
hash | |
.append("rect") | |
.attr("width", 3) | |
.attr("height", 8) | |
.attr("fill", mainColour); | |
let gradientStart = defs | |
.append("linearGradient") | |
.attr("id", "linearStart") | |
.attr("x1", 0) | |
.attr("y1", 0) | |
.attr("x2", "100%") | |
.attr("y2", 0); | |
let gradientBottom = defs | |
.append("linearGradient") | |
.attr("id", "linearBottom") | |
.attr("x1", 0) | |
.attr("y1", 0) | |
.attr("x2", 0) | |
.attr("y2", "100%"); | |
let gradientMiddle = defs | |
.append("linearGradient") | |
.attr("id", "linearMiddle") | |
.attr("x1", 0) | |
.attr("y1", 0) | |
.attr("x2", 0) | |
.attr("y2", "100%"); | |
gradientStart | |
.append("stop") | |
.attr("offset", "0%") | |
.attr("stop-color", bkdColour); | |
gradientStart | |
.append("stop") | |
.attr("offset", "100%") | |
.attr("stop-color", mainColour); | |
gradientBottom | |
.append("stop") | |
.attr("offset", "0%") | |
.attr("stop-color", mainColour); | |
gradientBottom | |
.append("stop") | |
.attr("offset", "50%") | |
.attr("stop-color", mainColour); | |
gradientBottom | |
.append("stop") | |
.attr("offset", "100%") | |
.attr("stop-color", bkdColour); | |
gradientMiddle | |
.append("stop") | |
.attr("offset", "0%") | |
.attr("stop-color", bkdColour); | |
gradientMiddle | |
.append("stop") | |
.attr("offset", "50%") | |
.attr("stop-color", mainColour); | |
gradientMiddle | |
.append("stop") | |
.attr("offset", "100%") | |
.attr("stop-color", mainColour); | |
let g = svg | |
.append("g") | |
.attr("transform", "translate(" + chartPadding + "," + chartPadding + ")"); | |
let rects = g | |
.selectAll("rect") | |
.data(graph.nodes) | |
.join("rect") | |
.attr("x", d => d.x0) | |
.attr("y", d => d.y0) | |
.attr("height", nodeHeight) | |
.attr("width", nodeWidth) | |
.style("stroke", bkdColour) | |
.style("fill", nodeFill); | |
const link = g | |
.append("g") | |
.selectAll("g") | |
.data(graph.links) | |
.join("g") | |
.append("path") | |
.attr("fill", linkFill) | |
.attr("stroke-width", linkStrokeWidth) | |
.style("stroke", linkStroke) | |
.attr("d", d => d.path); | |
let labels = g | |
.append("g") | |
.attr("font-family", "sans-serif") | |
.attr("font-size", 10) | |
.selectAll("text") | |
.data(graph.nodes) | |
.join("text") | |
.attr("x", d => d.labelX) | |
.attr("y", d => d.labelY) | |
.attr("text-anchor", d => d.labelAlign) | |
.text(d => d.name); | |
if (rotate) { | |
svg.attr("width", svgHeight).attr("height", svgWidth); | |
labels | |
.attr("transform", function (d) { | |
return "rotate(270, " + d.labelX + ", " + d.labelY + ")"; | |
}) | |
.attr("dy", "0.35em"); | |
let cx = chartHeight / 2 + chartPadding / 2; | |
let cy = chartHeight / 2 + chartPadding / 2; | |
g.attr( | |
"transform", | |
"translate(0, " + chartPadding + ") rotate(90, " + cx + ", " + cy + " )" | |
); | |
} | |
// document.body.appendChild(svg.node); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment