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

如何用D3.js实现类Highcharts的固定坐标3D图(含带权重弧线)

Hey there! Let's walk through how to build this 3D visualization in D3.js that matches the functionality you're looking for—fixed 3D coordinates, interactive rotation, colored nodes with hover details, and variable-width arcs between nodes. Here's a step-by-step implementation:

1. Setup the Core Environment & 3D Projection

D3 doesn’t have native 3D support, so we’ll start with a custom perspective projection to map your fixed X/Y/Z coordinates to 2D screen space. We’ll also set up the SVG container and basic interaction state.

const width = 800;
const height = 600;

// Create SVG container
const svg = d3.select("#visualization")
  .append("svg")
  .attr("width", width)
  .attr("height", height)
  .style("cursor", "grab");

// State for rotation
let rotationX = 0;
let rotationY = 0;
let isDragging = false;
let lastMousePos = { x: 0, y: 0 };

// Custom 3D-to-2D projection with rotation support
function project(x, y, z, rotX, rotY) {
  // Apply X-axis rotation
  const rotatedY = y * Math.cos(rotX) - z * Math.sin(rotX);
  const rotatedZ = y * Math.sin(rotX) + z * Math.cos(rotX);
  
  // Apply Y-axis rotation
  const rotatedX = x * Math.cos(rotY) + rotatedZ * Math.sin(rotY);
  const finalZ = -x * Math.sin(rotY) + rotatedZ * Math.cos(rotY);
  
  // Perspective projection (adjust focal length for depth effect)
  const focalLength = 500;
  const scale = focalLength / (focalLength + finalZ);
  
  return {
    x: width/2 + rotatedX * scale,
    y: height/2 + rotatedY * scale,
    z: finalZ // Store Z for occlusion sorting
  };
}
2. Render Fixed 3D Nodes

Next, we’ll render your pre-positioned nodes, add custom coloring, and set up hover tooltips for details. We’ll also adjust opacity based on Z-depth to enhance 3D perception.

// Sample node data (replace with your fixed coordinates)
const nodes = [
  { id: 1, x: 120, y: 60, z: 30, color: "#ff5733", details: "Node 1: Sensor A" },
  { id: 2, x: -90, y: -40, z: -15, color: "#33ff57", details: "Node 2: Sensor B" },
  { id: 3, x: 50, y: -70, z: 45, color: "#3357ff", details: "Node 3: Gateway" }
];

// Create tooltip for hover details
const tooltip = d3.select("body")
  .append("div")
  .style("position", "absolute")
  .style("background", "rgba(0,0,0,0.8)")
  .style("color", "white")
  .style("padding", "8px 12px")
  .style("border-radius", "4px")
  .style("display", "none")
  .style("pointer-events", "none");

// Render/update nodes
function renderNodes(rotX, rotY) {
  const nodeElements = svg.selectAll(".node")
    .data(nodes);

  nodeElements.enter()
    .append("circle")
    .attr("class", "node")
    .merge(nodeElements)
    .each(d => {
      d.proj = project(d.x, d.y, d.z, rotX, rotY);
    })
    .attr("cx", d => d.proj.x)
    .attr("cy", d => d.proj.y)
    .attr("r", 8)
    .attr("fill", d => d.color)
    .style("opacity", d => 1 - (d.proj.z / 600)) // Fade distant nodes
    .on("mouseover", (event, d) => {
      tooltip.style("display", "block")
        .style("left", `${event.pageX + 10}px`)
        .style("top", `${event.pageY - 10}px`)
        .html(d.details);
    })
    .on("mouseout", () => tooltip.style("display", "none"));

  nodeElements.exit().remove();
}
3. Draw Variable-Width Arcs Between Nodes

We’ll add curved arcs between nodes, with stroke width tied to your "strength" value. Critical here is sorting arcs by their average Z-depth to handle occlusion (farther arcs draw first, so closer ones appear on top).

// Sample link data (replace with your connections and strength values)
const links = [
  { source: 1, target: 2, strength: 6, color: "#999" },
  { source: 1, target: 3, strength: 12, color: "#666" },
  { source: 2, target: 3, strength: 4, color: "#aaa" }
];

// Render/update arcs
function renderLinks(rotX, rotY) {
  // Precompute projected positions for all nodes first
  nodes.forEach(d => {
    d.proj = project(d.x, d.y, d.z, rotX, rotY);
  });

  // Sort links by average Z-depth to handle occlusion
  const sortedLinks = [...links].sort((a, b) => {
    const zA = (nodes.find(n => n.id === a.source).proj.z + nodes.find(n => n.id === a.target).proj.z) / 2;
    const zB = (nodes.find(n => n.id === b.source).proj.z + nodes.find(n => n.id === b.target).proj.z) / 2;
    return zA - zB; // Draw farther links first
  });

  const linkElements = svg.selectAll(".link")
    .data(sortedLinks);

  linkElements.enter()
    .append("path")
    .attr("class", "link")
    .merge(linkElements)
    .attr("d", d => {
      const source = nodes.find(n => n.id === d.source).proj;
      const target = nodes.find(n => n.id === d.target).proj;
      // Create a curved arc using quadratic bezier
      const controlX = (source.x + target.x) / 2;
      const controlY = (source.y + target.y) / 2 - 30; // Adjust curve height here
      return `M ${source.x} ${source.y} Q ${controlX} ${controlY} ${target.x} ${target.y}`;
    })
    .attr("stroke", d => d.color)
    .attr("stroke-width", d => d.strength * 0.6) // Scale strength to visible width
    .attr("fill", "none")
    .style("opacity", d => {
      const zAvg = (nodes.find(n => n.id === d.source).proj.z + nodes.find(n => n.id === d.target).proj.z) / 2;
      return 1 - (zAvg / 600);
    });

  linkElements.exit().remove();
}
4. Add Interactive Rotation

Finally, we’ll hook up mouse drag events to update rotation angles and re-render the scene in real time, matching the drag-to-rotate behavior of the Highcharts example.

// Mouse drag handlers
svg.on("mousedown", event => {
  isDragging = true;
  lastMousePos = { x: event.clientX, y: event.clientY };
  svg.style("cursor", "grabbing");
});

svg.on("mousemove", event => {
  if (!isDragging) return;
  const deltaX = event.clientX - lastMousePos.x;
  const deltaY = event.clientY - lastMousePos.y;

  // Adjust rotation sensitivity (tweak these values as needed)
  rotationY += deltaX * 0.01;
  rotationX += deltaY * 0.01;

  lastMousePos = { x: event.clientX, y: event.clientY };

  // Re-render links first (since they're sorted by Z), then nodes
  renderLinks(rotationX, rotationY);
  renderNodes(rotationX, rotationY);
});

svg.on("mouseup", () => {
  isDragging = false;
  svg.style("cursor", "grab");
});

// Initial render
renderLinks(rotationX, rotationY);
renderNodes(rotationX, rotationY);
Quick Tips for Polish
  • Adjust the focalLength in the projection function to make the 3D effect more or less pronounced.
  • Tweak the curve height in the arc drawing code to make arcs more/less curved.
  • For smoother drag performance, wrap the re-render calls in requestAnimationFrame.

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.20 10:35:49