D3.js 曲线节点标签

D3.js curved node label

我想将当前节点标签替换为放置在节点上方的弯曲标签。我想念有关如何正确使用路径以及如何将文本附加到路径的知识。也许你们可以启发我。

文本应具有与节点本身相同的曲线。

更新

我实现了解决方案,但似乎路径或文本路径太短了。可以调整 innerRadius、outerRadius 以及 startAngle 和 endAngle。我想 startAngle 和 endAngle 定义了弧的长度。无论我测试哪个值,都不会显示完整标签。

<!DOCTYPE html>
<html lang="de">

<head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1">

    <title>Curved Text</title>

    <!-- Script Import -->
    <script src="https://d3js.org/d3.v7.js"></script>


</head>

<style>
    body {
        background: white;
        overflow: hidden;
        margin: 0px;
    }

    circle {
        fill: whitesmoke;
        stroke-width: 2;
        stroke: black;
        transform: scale(1);
        transition: all 200ms ease-in-out;
    }

    circle:hover {
        transform: scale(1.5)
    }
</style>

<body>
    <svg id="svg"></svg>
   
    <script>
        var width = window.innerWidth,
            height = window.innerHeight,
            radius = 40,    // circle radius
            offset = 35;    // arrow offset

        const svg = d3.select('svg')
            .attr("width", width)
            .attr("height", height)
            .call(d3.zoom().on("zoom", function (event) {
                svg.attr("transform", event.transform)
            }))
            .append("g")

        var graph = {
            "nodes": [
                { "id": "Whosebug" },
                { "id": "Reddit" },
                { "id": "Google" }
            ],
            "links": [
                { "source": "Whosebug", "target": "Reddit"},
                { "source": "Reddit", "target": "Google"},
                { "source": "Google", "target": "Whosebug"},
            ]
        }

        const arc = d3.arc()
            .innerRadius(radius + 5)
            .outerRadius(radius + 5)
            .startAngle(-Math.PI / 15)
            .endAngle(Math.PI / 2 )

        svg.append("defs")
            .append("path")
            .attr("id", "curvedLabelPath")
            .attr("d", arc())

        svg.append('defs').append('marker')
            .attr('id', 'arrowhead')
            .attr('viewBox', '-0 -5 10 10')
            .attr('refX', 0)
            .attr('refY', 0)
            .attr('orient', 'auto')
            .attr('markerWidth', 10)
            .attr('markerHeight', 10)
            .attr('xoverflow', 'visible')
            .append('svg:path')
            .attr('d', 'M 0,-5 L 10 ,0 L 0,5')
            .attr('fill', '#999')
            .style('stroke', 'none');

        var linksContainer = svg.append("g").attr("class", "linksContainer")
        var nodesContainer = svg.append("g").attr("class", "nodesContainer")

        const simulation = d3.forceSimulation()
            .force("link", d3.forceLink().id(function (d) { return d.id }).distance(250))
            .force("charge", d3.forceManyBody().strength(-1000))
            .force("center", d3.forceCenter(width / 2, height / 2))
            .force("collision", d3.forceCollide().radius(radius))

        link = linksContainer.selectAll("g")
            .data(graph.links)
            .join("g")
            .attr("curcor", "pointer")

        link = linksContainer.selectAll("path")
            .data(graph.links)
            .join("path")
            .attr("id", function (_, i) {
                return "path" + i
            })
            .attr("stroke", "#000000")
            .attr("opacity", 0.75)
            .attr("stroke-width", 3)
            .attr("fill", "transparent")
            .attr("marker-end", "url(#arrowhead)")

        node = nodesContainer.selectAll(".node")
            .data(graph.nodes, d => d.id)
            .join("g")
            .attr("cursor", "pointer")
            .call(d3.drag()
                .on("start", dragstarted)
                .on("drag", dragged)
                .on("end", dragended))
            .on("mouseenter", function (d) {
                d3.select(this).select("text").attr("font-size", 15)
            })

        node.selectAll("circle")
            .data(d => [d])
            .join("circle")
            .attr("r", radius)

        node.append("text")
            .append("textPath")
            .attr("href", "#curvedLabelPath")
            .attr("text-anchor", "middle")
            .attr("startoffset", "5%")
            .text(function (d) {
                return d.id
            })

        simulation
            .nodes(graph.nodes)
            .on("tick", ticked);

        simulation
            .force("link")
            .links(graph.links);

        function ticked() {
            link.attr("d", function (d) {
                var dx = (d.target.x - d.source.x),
                    dy = (d.target.y - d.source.y),
                    dr = Math.sqrt(dx * dx + dy * dy);
                return "M" + d.source.x + "," + d.source.y + "A" + dr + "," + dr + " 0 0,1 " + d.target.x + "," + d.target.y;
            });

            // Neuberechnung der Distanz
            link.attr("d", function (d) {
                // Länge des aktuellen Paths
                var pl = this.getTotalLength(),
                    // Kreis Radius und Distanzwert
                    r = radius + offset,
                    // Umlaufposition wo der Path den Kreis berührt
                    m = this.getPointAtLength(pl - r);

                var dx = m.x - d.source.x,
                    dy = m.y - d.source.y,
                    dr = Math.sqrt(dx * dx + dy * dy);

                return "M" + d.source.x + "," + d.source.y + "A" + dr + "," + dr + " 0 0,1 " + m.x + "," + m.y;
            });

            node
                .attr("transform", d => `translate(${d.x}, ${d.y})`);
        }

        function dragstarted(event, d) {
            if (!event.active) simulation.alphaTarget(0.3).restart();
            d.fx = d.x;
            d.fy = d.y;
        }

        function dragged(event, d) {
            d.fx = event.x;
            d.fy = event.y;
        }

        function dragended(event, d) {
            if (!event.active) simulation.alphaTarget(0);
            d.fx = null;
            d.fy = null;
        }

        dataFlow()

        function dataFlow() {
            var lines = linksContainer.selectAll("path")

            dataflow = window.setInterval(function () {
                lines.style("stroke-dashoffset", offset)
                    .style("stroke", "black")
                    .style("stroke-dasharray", 5)
                    .style("opacity", 0.5)
                offset -= 1
            }, 40)
            var offset = 1;
        }
    </script>
</body>

</html>

这是一个使用 <textPath>.

的圆圈和标签的示例

<!DOCTYPE html>
<html>

<head>
  <meta charset="UTF-8">
  <script src="https://d3js.org/d3.v7.js"></script>
</head>

<body>
  <div id="chart"></div>

  <script>
    // set up

    const width = 200;
    const height = 200;

    const svg = d3.select('#chart')
      .append('svg')
        .attr('width', width)
        .attr('height', height);

    // radius of the circle that the label is above
    const radius = 50;

    // arc generator for the curved path
    const arc = d3.arc()
      // add a bit of space so that the label
      // won't be right on the circle
      .innerRadius(radius + 5)
      .outerRadius(radius + 5)
      .startAngle(-Math.PI / 2)
      .endAngle(Math.PI / 2);

    // add the path that the label will follow to <defs>
    svg.append('defs')
      .append('path')
        .attr('id', 'curvedLabelPath')
        .attr('d', arc());

    // create a group for the circle and label
    const g = svg.append('g')
        .attr('transform', `translate(${width / 2},${height / 2})`);

    // draw the circle
    g.append('circle')
        .attr('stroke', 'black')
        .attr('fill', '#d3d3d3')
        .attr('r', radius);

    // draw the label
    g.append('text')
      .append('textPath')
        .attr('href', '#curvedLabelPath')
        // these two lines center along the arc.
        // the offset is 25% instead of 50% because d3.arc() creates
        // an arc that has an outer and inner part. in this case,
        // each parth is ~50% of the path, so the middle of the
        // outer arc is 25%
        .attr('text-anchor', 'middle')
        .attr('startOffset', '25%')
        .text('Whosebug');
  </script>
</body>

</html>

以下是对您更新后的代码所做的更改,以使标签不会被截断,如我在下面的评论中所述:

<!DOCTYPE html>
<html lang="de">

<head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1">

    <title>Curved Text</title>

    <!-- Script Import -->
    <script src="https://d3js.org/d3.v7.js"></script>


</head>

<style>
    body {
        background: white;
        overflow: hidden;
        margin: 0px;
    }

    circle {
        fill: whitesmoke;
        stroke-width: 2;
        stroke: black;
        transform: scale(1);
        transition: all 200ms ease-in-out;
    }

    circle:hover {
        transform: scale(1.5)
    }
</style>

<body>
    <svg id="svg"></svg>
   
    <script>
        var width = window.innerWidth,
            height = window.innerHeight,
            radius = 40,    // circle radius
            offset = 35;    // arrow offset

        const svg = d3.select('svg')
            .attr("width", width)
            .attr("height", height)
            .call(d3.zoom().on("zoom", function (event) {
                svg.attr("transform", event.transform)
            }))
            .append("g")

        var graph = {
            "nodes": [
                { "id": "Whosebug" },
                { "id": "Reddit" },
                { "id": "Google" }
            ],
            "links": [
                { "source": "Whosebug", "target": "Reddit"},
                { "source": "Reddit", "target": "Google"},
                { "source": "Google", "target": "Whosebug"},
            ]
        }

        const arc = d3.arc()
            .innerRadius(radius + 5)
            .outerRadius(radius + 5)
            .startAngle(-Math.PI / 2)
            .endAngle(Math.PI / 2 )

        svg.append("defs")
            .append("path")
            .attr("id", "curvedLabelPath")
            .attr("d", arc())

        svg.append('defs').append('marker')
            .attr('id', 'arrowhead')
            .attr('viewBox', '-0 -5 10 10')
            .attr('refX', 0)
            .attr('refY', 0)
            .attr('orient', 'auto')
            .attr('markerWidth', 10)
            .attr('markerHeight', 10)
            .attr('xoverflow', 'visible')
            .append('svg:path')
            .attr('d', 'M 0,-5 L 10 ,0 L 0,5')
            .attr('fill', '#999')
            .style('stroke', 'none');

        var linksContainer = svg.append("g").attr("class", "linksContainer")
        var nodesContainer = svg.append("g").attr("class", "nodesContainer")

        const simulation = d3.forceSimulation()
            .force("link", d3.forceLink().id(function (d) { return d.id }).distance(250))
            .force("charge", d3.forceManyBody().strength(-1000))
            .force("center", d3.forceCenter(width / 2, height / 2))
            .force("collision", d3.forceCollide().radius(radius))

        link = linksContainer.selectAll("g")
            .data(graph.links)
            .join("g")
            .attr("curcor", "pointer")

        link = linksContainer.selectAll("path")
            .data(graph.links)
            .join("path")
            .attr("id", function (_, i) {
                return "path" + i
            })
            .attr("stroke", "#000000")
            .attr("opacity", 0.75)
            .attr("stroke-width", 3)
            .attr("fill", "transparent")
            .attr("marker-end", "url(#arrowhead)")

        node = nodesContainer.selectAll(".node")
            .data(graph.nodes, d => d.id)
            .join("g")
            .attr("cursor", "pointer")
            .call(d3.drag()
                .on("start", dragstarted)
                .on("drag", dragged)
                .on("end", dragended))
            .on("mouseenter", function (d) {
                d3.select(this).select("text").attr("font-size", 15)
            })

        node.selectAll("circle")
            .data(d => [d])
            .join("circle")
            .attr("r", radius)

        node.append("text")
            .append("textPath")
            .attr("href", "#curvedLabelPath")
            .attr("text-anchor", "middle")
            .attr("startOffset", "25%")
            .text(function (d) {
                return d.id
            })

        simulation
            .nodes(graph.nodes)
            .on("tick", ticked);

        simulation
            .force("link")
            .links(graph.links);

        function ticked() {
            link.attr("d", function (d) {
                var dx = (d.target.x - d.source.x),
                    dy = (d.target.y - d.source.y),
                    dr = Math.sqrt(dx * dx + dy * dy);
                return "M" + d.source.x + "," + d.source.y + "A" + dr + "," + dr + " 0 0,1 " + d.target.x + "," + d.target.y;
            });

            // Neuberechnung der Distanz
            link.attr("d", function (d) {
                // Länge des aktuellen Paths
                var pl = this.getTotalLength(),
                    // Kreis Radius und Distanzwert
                    r = radius + offset,
                    // Umlaufposition wo der Path den Kreis berührt
                    m = this.getPointAtLength(pl - r);

                var dx = m.x - d.source.x,
                    dy = m.y - d.source.y,
                    dr = Math.sqrt(dx * dx + dy * dy);

                return "M" + d.source.x + "," + d.source.y + "A" + dr + "," + dr + " 0 0,1 " + m.x + "," + m.y;
            });

            node
                .attr("transform", d => `translate(${d.x}, ${d.y})`);
        }

        function dragstarted(event, d) {
            if (!event.active) simulation.alphaTarget(0.3).restart();
            d.fx = d.x;
            d.fy = d.y;
        }

        function dragged(event, d) {
            d.fx = event.x;
            d.fy = event.y;
        }

        function dragended(event, d) {
            if (!event.active) simulation.alphaTarget(0);
            d.fx = null;
            d.fy = null;
        }

        dataFlow()

        function dataFlow() {
            var lines = linksContainer.selectAll("path")

            dataflow = window.setInterval(function () {
                lines.style("stroke-dashoffset", offset)
                    .style("stroke", "black")
                    .style("stroke-dasharray", 5)
                    .style("opacity", 0.5)
                offset -= 1
            }, 40)
            var offset = 1;
        }
    </script>
</body>

</html>