Skip to content

Instantly share code, notes, and snippets.

@promethyttrium
Created July 14, 2023 21:56
Show Gist options
  • Save promethyttrium/6480217833998c10c6290e464b12840c to your computer and use it in GitHub Desktop.
Save promethyttrium/6480217833998c10c6290e464b12840c to your computer and use it in GitHub Desktop.
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