I am trying to create a force graph using javascript within an R shiny application. The code for the app is below.
library(shiny)
library(jsonlite)
network_data <- jsonlite::read_json("miserables.json")
ui <- fluidPage(
tags$script(src='https://d3js.org/d3.v6.min.js'),
titlePanel("force graph example - miserables"),
sidebarLayout(
sidebarPanel(
actionButton(
"go",
label = "go"
)
),
mainPanel(
p("yo"),
uiOutput("output_result")
)
)
)
# Define server logic required to draw a histogram
server <- function(input, output, session) {
observeEvent(input$go, {
session$sendCustomMessage(type = "jsondata",network_data)
},ignoreNULL = TRUE,ignoreInit = TRUE)
output$output_result <- renderUI({
result <- HTML('<script type="text/javascript", src="fn_canvas.js"> </script>')
return(result)
})
}
# Run the application
shinyApp(ui = ui, server = server)
The application is pretty simple. You hit the go button and it sends the miserables JSON to a javascript file, called fn_canvas.js, which should create the force graph. The code for the force graph can be found here which I pulled and created this javascript file.
Shiny.addCustomMessageHandler('jsondata', function(message) {
console.log("helllooooo")
var network_data = message;
function ForceGraph({
nodes, // an iterable of node objects (typically [{id}, …])
links // an iterable of link objects (typically [{source, target}, …])
}, {
nodeId = d => d.id, // given d in nodes, returns a unique identifier (string)
nodeGroup, // given d in nodes, returns an (ordinal) value for color
nodeGroups, // an array of ordinal values representing the node groups
nodeFill = "currentColor", // node stroke fill (if not using a group color encoding)
nodeStroke = "#fff", // node stroke color
nodeStrokeWidth = 1.5, // node stroke width, in pixels
nodeStrokeOpacity = 1, // node stroke opacity
nodeRadius = 5, // node radius, in pixels
nodeStrength,
linkSource = ({source}) => source, // given d in links, returns a node identifier string
linkTarget = ({target}) => target, // given d in links, returns a node identifier string
linkStroke = "#999", // link stroke color
linkStrokeOpacity = 0.6, // link stroke opacity
linkStrokeWidth = 1.5, // given d in links, returns a stroke width in pixels
linkStrokeLinecap = "round", // link stroke linecap
linkStrength,
colors = d3.schemeTableau10, // an array of color strings, for the node groups
width = 640, // outer width, in pixels
height = 400
///, // outer height, in pixels
///invalidation // when this promise resolves, stop the simulation,
} = {}) {
// Compute values.
const N = d3.map(nodes, nodeId).map(intern);
const LS = d3.map(links, linkSource).map(intern);
const LT = d3.map(links, linkTarget).map(intern);
const G = nodeGroup == null ? null : d3.map(nodes, nodeGroup).map(intern);
const W = typeof linkStrokeWidth !== "function" ? null : d3.map(links, linkStrokeWidth);
const L = typeof linkStroke !== "function" ? null : d3.map(links, linkStroke);
// Replace the input nodes and links with mutable objects for the simulation.
nodes = d3.map(nodes, (_, i) => ({id: N[i]}));
links = d3.map(links, (_, i) => ({source: LS[i], target: LT[i]}));
// Compute default domains.
if (G && nodeGroups === undefined) nodeGroups = d3.sort(G);
// Construct the scales.
const color = nodeGroup == null ? null : d3.scaleOrdinal(nodeGroups, colors);
// Construct the forces.
const forceNode = d3.forceManyBody();
const forceLink = d3.forceLink(links).id(({index: i}) => N[i]);
if (nodeStrength !== undefined) forceNode.strength(nodeStrength);
if (linkStrength !== undefined) forceLink.strength(linkStrength);
const simulation = d3.forceSimulation(nodes)
.force("link", forceLink)
.force("charge", forceNode)
.force("center", d3.forceCenter(width/2, height/2))
.on("tick", ticked);
/// Is it ok to do the below??!
function context2d(width, height, dpi) {
if (dpi == null) dpi = devicePixelRatio;
var canvas = document.createElement("canvas");
canvas.width = width * dpi;
canvas.height = height * dpi;
canvas.style.width = width + "px";
var context = canvas.getContext("2d");
context.scale(dpi, dpi);
return context;
}
const context = context2d(width, height);
///const context = DOM.context2d(width, height);
function ticked() {
context.clearRect(0, 0, width, height);
context.save();
context.globalAlpha = linkStrokeOpacity;
for (const [i, link] of links.entries()) {
context.beginPath();
drawLink(link);
context.strokeStyle = L ? L[i]: linkStroke;
context.lineWidth = W ? W[i]: linkStrokeWidth;
context.stroke();
}
context.restore();
context.save();
context.strokeStyle = nodeStroke;
context.globalAlpha = nodeStrokeOpacity;
for (const [i, node] of nodes.entries()) {
context.beginPath();
drawNode(node)
context.fillStyle = G ? color(G[i]): nodeFill;
context.strokeStyle = nodeStroke;
context.fill();
context.stroke();
}
context.restore();
}
function drawLink(d) {
context.moveTo(d.source.x, d.source.y);
context.lineTo(d.target.x, d.target.y);
}
function drawNode(d) {
context.moveTo(d.x + nodeRadius, d.y);
context.arc(d.x, d.y, nodeRadius, 0, 2 * Math.PI);
}
///if (invalidation != null) invalidation.then(() => simulation.stop());
function intern(value) {
return value !== null && typeof value === "object" ? value.valueOf() : value;
}
function drag(simulation) {
function dragstarted(event) {
if (!event.active) simulation.alphaTarget(0.3).restart();
event.subject.fx = event.subject.x;
event.subject.fy = event.subject.y;
}
function dragged(event) {
event.subject.fx = event.x;
event.subject.fy = event.y;
}
function dragended(event) {
if (!event.active) simulation.alphaTarget(0);
event.subject.fx = null;
event.subject.fy = null;
}
function dragsubject(event) {
return simulation.find(event.sourceEvent.offsetX, event.sourceEvent.offsetY);
}
return d3.drag()
.subject(dragsubject)
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended);
}
console.log("finished")
return Object.assign(d3.select(context.canvas).call(drag(simulation)).node(), {scales: {color}});
}
chart = ForceGraph(network_data, {
nodeId: (d) => d.id,
nodeGroup: (d) => d.group,
linkStrokeWidth: (l) => Math.sqrt(l.value),
width: 600,
height: 600
///,
///invalidation, // a promise to stop the simulation when the cell is re-run
})
return chart
});
Once you hit the go button in the application, the force graph should be returned from the javascript file. However, currently nothing is returned from the file. I even print console messages which say the code was run. I’m not sure whether there is an issue with my R code or the javascript code. Any help appreciated. Thanks for reading.