You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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
  
});

问题分析与解决方案

核心问题

  1. Canvas未挂载到DOM:JS代码中创建了Canvas元素,但没有将它添加到页面的DOM结构里,导致绘制内容无法在页面显示。
  2. 脚本加载顺序错误:当前逻辑是点击按钮后才加载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);
    
    // ... 保留原函数内的其余逻辑
  }
  
  // ... 保留原调用逻辑
});

验证修改

  1. 确保miserables.json文件与Shiny应用脚本(app.R)在同一目录下。
  2. 运行修改后的应用,点击「go」按钮,即可在主面板看到渲染完成的力导向图。

内容的提问来源于stack exchange,提问作者AyeTown

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.27 20:17:13