Skip to content

Commit

Permalink
modernized sankey (#9)
Browse files Browse the repository at this point in the history
  • Loading branch information
cjyetman authored Dec 21, 2023
1 parent 403f0b7 commit 7977657
Show file tree
Hide file tree
Showing 2 changed files with 137 additions and 111 deletions.
17 changes: 2 additions & 15 deletions R/sankey_network.R
Original file line number Diff line number Diff line change
Expand Up @@ -22,28 +22,15 @@ sankey_network <- function(data, width = NULL, height = NULL, ..., viewer = "int
UTC = TRUE, rownames = FALSE, keep_vec_names = TRUE,
json_verabitm = TRUE)

options <- list(
linkStrokeOpacity = ifelse(methods::hasArg("linkStrokeOpacity"), list(...)$linkStrokeOpacity, 0.3),
linkMixBlendMode = ifelse(methods::hasArg("linkMixBlendMode"), list(...)$linkMixBlendMode, "multiply"),
linkPath = ifelse(methods::hasArg("linkPath"), list(...)$linkPath, "d3.sankeyLinkHorizontal()"),
linkColor = ifelse(methods::hasArg("linkColor"), list(...)$linkColor, "source-target"),
nodeAlign = ifelse(methods::hasArg("nodeAlign"), list(...)$nodeAlign, "justify"),
nodeGroup = ifelse(methods::hasArg("nodeGroup"), list(...)$nodeGroup, "group"),
nodeWidth = ifelse(methods::hasArg("nodeWidth"), list(...)$nodeWidth, 15),
nodePadding = ifelse(methods::hasArg("nodePadding"), list(...)$nodePadding, 10),
nodeLabelPadding = ifelse(methods::hasArg("nodeLabelPadding"), list(...)$nodeLabelPadding, 6),
nodeLabelFontFamily = ifelse(methods::hasArg("nodeLabelFontFamily"), list(...)$nodeLabelFontFamily, "sans-serif"),
nodeLabelFontSize = ifelse(methods::hasArg("nodeLabelFontSize"), list(...)$nodeLabelFontSize, 10),
colors = ifelse(methods::hasArg("color"), list(...)$color, "d3.schemeCategory10")
)
options <- list(...)

r2d3::r2d3(
data = data,
options = options,
script = system.file("sankey_network.js", package = "network.r2d3"),
dependencies = system.file("lib/d3-sankey/d3-sankey.min.js", package = "network.r2d3"),
d3_version = 6,
container = "svg",
container = "div",
width = width,
height = height,
viewer = viewer
Expand Down
231 changes: 135 additions & 96 deletions inst/sankey_network.js
Original file line number Diff line number Diff line change
@@ -1,107 +1,146 @@
// !preview r2d3 data = jsonlite::toJSON(list(nodes=data.frame(id=c(0,1,2,3,4,5,6),name=c("node0","node1","node2","node3","node4","node5","node6"),group=c("grp1","grp1","grp2","grp2","grp2","grp3","grp3")),links=data.frame(source=c(0,1,1,1,0,2,2,3,5),target=c(2,2,3,5,4,3,4,4,6),value=c(2,2,2,2,2,2,2,4,4)))), dependencies = "inst/lib/d3-sankey/d3-sankey.min.js", d3_version = 6, width = 600, height = 300, options = list(linkStrokeOpacity=0.3,linkMixBlendMode="multiply",linkPath="d3.sankeyLinkHorizontal()",linkColor="source-target",nodeAlign="justify",nodeGroup="group",nodeWidth=15,nodePadding=10,nodeLabelPadding=6,nodeLabelFontFamily="sans-serif",nodeLabelFontSize=10,colors="d3.schemeCategory10"), viewer = "internal"

r2d3.onRender(function(data, svg, width, height, options) {
let linkStrokeOpacity = options.linkStrokeOpacity;
let linkMixBlendMode = options.linkMixBlendMode;
let linkPath = eval(options.linkPath);
let linkColor = options.linkColor;
let nodeAlign = options.nodeAlign;
let nodeGroup = options.nodeGroup;
let nodeWidth = options.nodeWidth;
let nodePadding = options.nodePadding;
let nodeLabelPadding = options.nodeLabelPadding;
let nodeLabelFontFamily = options.nodeLabelFontFamily;
let nodeLabelFontSize = options.nodeLabelFontSize;
let colors = eval(options.colors);

const uid = `O-${Math.random().toString(16).slice(2)}`;

nodeAlign = {
left: d3.sankeyLeft,
right: d3.sankeyRight,
center: d3.sankeyCenter
}[nodeAlign] ?? d3.sankeyJustify;

const formatNumber = d3.format(",.0f");

const color = d3.scaleOrdinal(colors);

let sankey = d3.sankey()
.nodeId(function id(d) { return d.id; })
// !preview r2d3 data = jsonlite::toJSON(list(links = data.frame(source = c("A", "A"), target = c("B", "C"), value = 10), nodes = data.frame(name = c("A", "B", "C"), group = c("A", "B", "C")))), dependencies = "inst/lib/d3-sankey/d3-sankey.min.js", d3_version = 6, options = list(linkStrokeOpacity=0.3,nodeLabelPadding=6), container = "div", viewer = "internal"

r2d3.onRender(function(data, div, width, height, options) {

const nodeAlign = options.nodeAlign ?? "sankeyJustify";
const nodeWidth = options.nodeWidth ?? 24;
const nodePadding = options.nodePadding ?? 8;
const nodeGroup = options.nodeGroup ?? "group";
const colorScheme = options.colorScheme ?? "schemeCategory10";
const linkColor = options.linkColor ?? "source-target";
const nodeLabelFontFamily = options.nodeLabelFontFamily ?? "sans-serif";
const nodeLabelFontSize = options.nodeLabelFontSize ?? 10;
const tooltipTransitionDuration = options.tooltipTransitionDuration ?? 200;
const tooltipOpacity = options.tooltipOpacity ?? 0.8;
const tooltipFontSize = options.tooltipFontSize ?? 12;
const tooltipFontFamily = options.tooltipFontFamily ?? "sans-serif";
const tooltipBorderRadius = options.tooltipBorderRadius ?? 4;

const color = d3.scaleOrdinal(d3[colorScheme]);

const widgetPadding = 40;

const format = d3.format(",.0f");

const svg = div.append("svg")
.attr("width", width)
.attr("height", height)
.attr("viewBox", [0, 0, width, height])
.attr("style", "max-width: 100%; height: auto;");

const sankey = d3.sankey()
.nodeId(d => d.name)
.nodeAlign(d3[nodeAlign])
.nodeWidth(nodeWidth)
.nodeAlign(nodeAlign)
.nodePadding(nodePadding)
.size([width, height])
;

let sankeydata = sankey(data);
let links = sankeydata.links;
let nodes = sankeydata.nodes;

// add in the links
const link_slct = svg
.append("g")
.attr("fill", "none")
.attr("stroke-opacity", linkStrokeOpacity)
.selectAll("g")
.extent([[1, 5], [width - 1, height - 5]]);

const {nodes, links} = sankey({
nodes: data.nodes.map(d => Object.assign({}, d)),
links: data.links.map(d => Object.assign({}, d))
});

// add tooltip div
const tooltip_div = div.append("div")
.attr("class", "tooltip")
.style("opacity", 0)
.style("position", "absolute")
.style("text-align", "center")
.style("padding", "10px")
.style("font-size", tooltipFontSize + "px")
.style("font-family", tooltipFontFamily)
.style("background-color", "white")
.style("color", "black")
.style("border", "1px solid")
.style("border-radius", tooltipBorderRadius + "px")
.style("pointer-events", "none");

function mouseover(event, d) {
let tooltip_text = "";
if (d.name === undefined) {
tooltip_text = d.source.name + " → " + d.target.name + "<br/>" + format(d.value);
} else {
tooltip_text = d.name + "<br/>" + format(d.value);
}
tooltip_div.transition()
.duration(tooltipTransitionDuration)
.style("opacity", tooltipOpacity);
tooltip_div.html(tooltip_text)
.style("left", event.pageX + "px")
.style("top", (event.pageY - widgetPadding) + "px");
}

function mousemove(event) {
tooltip_div
.style("left", event.pageX + "px")
.style("top", (event.pageY - widgetPadding) + "px");
}

function mouseout() {
tooltip_div.transition()
.duration(tooltipTransitionDuration)
.style("opacity", 0);
}

// build nodes
svg.append("g")
.attr("stroke", "#000")
.selectAll()
.data(nodes)
.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[nodeGroup]))
.on("mouseover", mouseover)
.on("mousemove", mousemove)
.on("mouseout", mouseout);

svg.append("g")
.selectAll()
.data(nodes)
.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")
.text(d => d.name)
.style("font-size", nodeLabelFontSize + "px")
.style("font-family", nodeLabelFontFamily);

// build links
const link = svg.append("g")
.attr("fill", "none")
.attr("stroke-opacity", 0.5)
.selectAll()
.data(links)
.join("g")
.style("mix-blend-mode", linkMixBlendMode)
;
.style("mix-blend-mode", "multiply");

if (linkColor === "source-target") {
link_slct.append("linearGradient")
.attr("id", d => `${uid}-link-${d.index}`)
.attr("gradientUnits", "userSpaceOnUse")
.attr("x1", d => d.source.x1)
.attr("x2", d => d.target.x0)
.call(gradient => gradient.append("stop")
const gradient = link.append("linearGradient")
.attr("id", (d, i) => (d.uid = `link-${i}`))
.attr("gradientUnits", "userSpaceOnUse")
.attr("x1", d => d.source.x1)
.attr("x2", d => d.target.x0);
gradient.append("stop")
.attr("offset", "0%")
.attr("stop-color", d => color(d.source[nodeGroup]))
)
.call(gradient => gradient.append("stop")
.attr("stop-color", d => color(d.source[nodeGroup]));
gradient.append("stop")
.attr("offset", "100%")
.attr("stop-color", d => color(d.target[nodeGroup]))
)
;
.attr("stop-color", d => color(d.target[nodeGroup]));
}

link_slct.append("path")
.attr("d", linkPath)
.attr("stroke", linkColor === "source-target" ? d => `url(#${uid}-link-${d.index})`
: linkColor === "source" ? d => color(d.source[nodeGroup])
: linkColor === "target" ? d => color(d.target[nodeGroup])
: linkColor)
.attr("stroke-width", ({width}) => Math.max(1, width))
.append("title")
.text(d => d.source.name + " → " + d.target.name + "\n" + formatNumber(d.value))
;

const node_slct = svg.append("g")
.selectAll(".node")
.data(nodes)
.join("rect")
.attr("class", "node")
.attr("x", d => d.x0)
.attr("y", d => d.y0)
.attr("height", d => d.y1 - d.y0)
.attr("width", sankey.nodeWidth())
.style("fill", d => d.color = color(d[nodeGroup]))
.style("stroke", d => d3.rgb(d.color).darker(2))
;

node_slct.append("title").text(d => d.name + "\n" + formatNumber(d.value));

const nodeLabel_slct = svg.append("g")
.attr("font-family", nodeLabelFontFamily)
.attr("font-size", nodeLabelFontSize)
.selectAll("text")
.data(nodes)
.join("text")
.attr("x", d => d.x0 < width / 2 ? d.x1 + nodeLabelPadding : d.x0 - nodeLabelPadding)
.attr("y", d => (d.y1 + d.y0) / 2)
.attr("dy", "0.35em")
.attr("text-anchor", d => d.x0 < width / 2 ? "start" : "end")
.text(d => d.name)
;
link.append("path")
.attr("d", d3.sankeyLinkHorizontal())
.attr("stroke", linkColor === "source-target" ? (d) => `url(#${d.uid})`
: linkColor === "source" ? (d) => color(d.source[nodeGroup])
: linkColor === "target" ? (d) => color(d.target[nodeGroup])
: linkColor)
.attr("stroke-width", d => Math.max(1, d.width))
.on("mouseover", mouseover)
.on("mousemove", mousemove)
.on("mouseout", mouseout);

});

0 comments on commit 7977657

Please sign in to comment.