js实现不改变节点角度的平移

296 阅读1分钟

已d3js为例。已知力导向图线条坐标,实现使节点不改变角度平移

原始效果

捕获.PNG

要实现的效果

捕获1.PNG

实现思路:
  1. 每条线把它想成是直角三角形
  2. 计算出三角形对边边长和邻边边长。
  3. 反正切(Math.atan2)计算出夹角的弧度。
  4. 使用弧度计算正、余弦。x轴使用余弦值,y轴使用正弦值
  5. cos or sin(正、余弦值) * size(大小) + center(中心点坐标) 捕获.PNG
公式实现:
弧度 = Math.atan2(y2 - y1(对边长), x2 - x1(邻边长))
余弦值 = Math.cos(弧度)
正弦值 = Math.sin(弧度)
坐标 = cos or sin * size + center
完整代码
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<div></div>
<script src="https://d3js.org/d3.v6.min.js"></script>
<script>
    let dataset = {
        nodes: [
            {name: "Adam"},
            {name: "Bob"},
            {name: "Carrie"},
            {name: "Donovan"},
            {name: "Edward"},
            {name: "Felicity"},
            {name: "George"},
            {name: "Hannah"},
            {name: "Iris"},
            {name: "Jerry"}
        ],
        edges: [
            {source: 0, target: 1},
            {source: 0, target: 2},
            {source: 0, target: 3},
            {source: 0, target: 4},
            {source: 0, target: 5},
            {source: 0, target: 6},
            {source: 0, target: 7},
            {source: 0, target: 8},
            {source: 0, target: 9},
        ]
    };

    let w = 800
    let h = 800
    let colors = d3.scaleOrdinal(d3.schemeCategory10);

    let force = d3.forceSimulation(dataset.nodes)
        .force('charge', d3.forceManyBody().strength(-400))
        .force('link', d3.forceLink(dataset.edges).distance(120))
        .force('center', d3.forceCenter().x(w / 2).y(h / 2))

    let svg = d3.select('div')
        .append('svg')
        .attr('width', w)
        .attr('height', h)

    let defs = svg.append('defs')

    // 圆形图片
    defs.selectAll('pattern')
        .data(dataset.nodes)
        .enter()
        .append('pattern')
        .attr('id', function (d, i) {
            return `circleImage${i}`
        })
        .attr('patternUnits', 'objectBoundingBox')
        .attr('width', '100%')
        .attr('height', '100%')
        .append('image')
        .attr('preserveAspectRatio', 'none')
        .attr('height', f)
        .attr('width', f)
        .attr('xlink:href', 'test.jpg')  // 图片路径

    let edges = svg.selectAll('line')
        .data(dataset.edges)
        .enter()
        .append('line')
        .style('stroke-width', 2)
        .attr('stroke', 'red')

    function f(d, i) {
        if (i) {
            return 50
        } else {
            return 100
        }
    }

    let circle = svg.selectAll('circle')
        .data(dataset.nodes)
        .enter()
        .append('circle')
        .attr('r', function (d, i) {
            if (i) {
                return 25
            } else {
                return 50
            }
        })
        .attr('fill', function (d, i) {
            return `url(#circleImage${i})`
        })
        .attr('stroke-width', 2)
        .text((d) => d.name)

    let pathX = svg.append('line')
        .attr('stroke-width', 2)

    let pathY = svg.append('line')
        .attr('stroke-width', 2)

    let radText = svg.append('text')
        .attr('y', 100)

    let cosText = svg.append('text')
        .attr('y', 120)

    let sinText = svg.append('text')
        .attr('y', 140)

    let angle = svg.append('text')
        .attr('y', 160)


    force.on('tick', () => {
        edges.attr('x1', (d) => d.source.x)
            .attr('y1', (d) => d.source.y)
            .attr('x2', (d) => d.target.x)
            .attr('y2', (d) => d.target.y)


        circle.attr('cx', function (d, i) {
            if (i) {
                // y = 对边边长,x = 邻边边长
                // y2 - y1 = 对边长 , x2 - x1 = 邻边长
                let rad = Math.atan2(d.y - h / 2, d.x - w / 2); 
                let x = Math.cos(rad)

                // 正、余弦值 * size + 中心点坐标
                return x * 175 + w / 2
            }
            return d.x
        }).attr('cy', function (d, i) {
            this.setAttribute('stroke', '#45C3FF')
            if (i) {
                let rad = Math.atan2(d.y - h / 2, d.x - w / 2); 
                let y = Math.sin(rad);

                return y * 175 + h / 2
            } else {
                this.setAttribute('stroke-width', 3)
                return d.y - 50
            }
        }).on('mouseover', function (e, d) {
            let rad = Math.atan2(d.y - h / 2, d.x - w / 2); 
            let cos = Math.cos(rad);
            let sin = Math.sin(rad);

            pathX.attr('x1', w / 2)
                .attr('y1', h / 2)
                .attr('x2', (w / 2) + (d.x - w / 2))
                .attr('y2', h / 2)
                .attr('stroke', '#000000')

            pathY.attr('x1', (w / 2) + (d.x - w / 2))
                .attr('y1', h / 2)
                .attr('x2', d.x)
                .attr('y2', d.y)
                .attr('stroke', '#000000')

            radText.text(`${rad}(弧度)`)
                .attr('x', function () {
                    return w / 2 - this.getBBox().width / 2
                })

            cosText.text(`${cos}(余弦)`)
                .attr('x', function () {
                    return w / 2 - this.getBBox().width / 2
                })

            sinText.text(`${sin}(正弦)`)
                .attr('x', function () {
                    return w / 2 - this.getBBox().width / 2
                })

            let angel = 360 / (Math.PI * 2) * rad

            angle.text(`${angel > 0 ? angel : angel + 360}(夹角度数)`)
                .attr('x', function () {
                    return w / 2 - this.getBBox().width / 2
                })
        })
    })
</script>
</body>
</html>