diff --git a/R/sankey_network.R b/R/sankey_network.R
index 0398937..958e65b 100644
--- a/R/sankey_network.R
+++ b/R/sankey_network.R
@@ -22,20 +22,7 @@ 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,
@@ -43,7 +30,7 @@ sankey_network <- function(data, width = NULL, height = NULL, ..., viewer = "int
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
diff --git a/inst/sankey_network.js b/inst/sankey_network.js
index 7bb9f93..0904c35 100644
--- a/inst/sankey_network.js
+++ b/inst/sankey_network.js
@@ -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 + "
" + format(d.value);
+ } else {
+ tooltip_text = d.name + "
" + 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);
+
});