R Shiny中renderUI无法从JavaScript返回力导向图问题排查
问题描述
我在R Shiny应用中尝试用JavaScript(基于D3.js v6)创建力导向图,点击「go」按钮后,Shiny会把miserables.json数据发送到fn_canvas.js文件,这个JS文件是基于Observable的力导向图Canvas代码编写的。控制台显示代码已执行,但页面没有渲染出力导向图,不确定是R代码还是JavaScript代码有问题,求技术支持。
R Shiny 代码
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)
JavaScript 代码(fn_canvas.js)
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 });
问题分析与解决方案
核心问题
- Canvas未挂载到DOM:JS代码中创建了Canvas元素,但没有将它添加到页面的DOM结构里,导致绘制内容无法在页面显示。
- 脚本加载顺序错误:当前逻辑是点击按钮后才加载
fn_canvas.js,但此时Shiny已经发送了数据,消息处理器还未注册,会错过数据接收。
修复步骤
1. 修改R代码:提前加载JS并指定图形容器
将JS文件加载逻辑移到UI初始化阶段,同时添加一个固定容器用于放置Canvas:
ui <- fluidPage( tags$script(src='https://d3js.org/d3.v6.min.js'), tags$script(src='fn_canvas.js'), # 页面加载时就注册消息处理器 titlePanel("force graph example - miserables"), sidebarLayout( sidebarPanel( actionButton("go", label = "go") ), mainPanel( p("yo"), div(id = "graph-container") # 固定容器,用于挂载Canvas ) ) ) server <- function(input, output, session) { observeEvent(input$go, { session$sendCustomMessage(type = "jsondata", network_data) }, ignoreNULL = TRUE, ignoreInit = TRUE) }
2. 修改JavaScript代码:将Canvas添加到页面容器
在创建Canvas后,立即将其挂载到指定的DOM容器中:
Shiny.addCustomMessageHandler('jsondata', function(message) { console.log("helllooooo") var network_data = message; function ForceGraph({ nodes, links }, options = {}) { // ... 保留原函数内的所有逻辑 const context = context2d(width, height); // 新增:将Canvas挂载到页面容器 document.getElementById('graph-container').appendChild(context.canvas); // ... 保留原函数内的其余逻辑 } // ... 保留原调用逻辑 });
验证修改
- 确保
miserables.json文件与Shiny应用脚本(app.R)在同一目录下。 - 运行修改后的应用,点击「go」按钮,即可在主面板看到渲染完成的力导向图。
内容的提问来源于stack exchange,提问作者AyeTown
相关产品推荐
相关产品推荐

