如何用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:
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 }; }
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(); }
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(); }
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);
- Adjust the
focalLengthin 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

