d3js 图例自动换行排列算法

515 阅读1分钟

普通换行模式效果 捕获.PNG

换行均匀分布模式效果 捕获2.PNG

逻辑介绍

  1. x坐标计算:如图例数据为数组,数组下标为0的x坐标默认为0(按实际开发来设置初始坐标),到数组下标为1的时候获取上一个兄弟元素的x坐标 + 上一个兄弟元素的宽度 = 当前元素的x坐标,后面的循环依此类推 image.png
  2. x坐标超出重置逻辑:上一个兄弟元素的宽度 + 上一个兄弟元素的x坐标 + 当前元素的宽度 如大于 父级宽度(按实际开发来设置宽度)表示当前循环到的元素已经超出了父级的宽度,当前循环到的元素x坐标重置为0。 image.png
  3. y坐标换行计算:在attr('y')的循环中判断当前循环元素的x坐标是否小于等于0,如果小于等于0表示可以对y进行累计 image.png

实际使用中需要把rect.node().getBBox().width替换为自己设定的宽度

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<div>
    <button onclick="defaultMode()">普通换行模式</button>
    <button onclick="equallyMode()">换行均匀分布模式</button>
</div>
<script src="https://d3js.org/d3.v6.min.js"></script>
<script>
    let data = [
        {
            num: 10,
            label: '三星S'
        }, {
            num: 10,
            label: '三星Note'
        }, {
            num: 10,
            label: '华为Nova'
        }, {
            num: 10,
            label: '华为Mate'
        }, {
            num: 10,
            label: '三星W'
        }, {
            num: 10,
            label: '小米Note'
        }, {
            num: 10,
            label: '小米Mix'
        }, {
            num: 10,
            label: '小米Max'
        }, {
            num: 10,
            label: '小米Redmi'
        }, {
            num: 10,
            label: 'gbxvxs32'
        }, {
            num: 10,
            label: '989898'
        }, {
            num: 10,
            label: '32323232'
        }, {
            num: 10,
            label: 'efegfsgds'
        }, {
            num: 10,
            label: 'vfxcvffv'
        }, {
            num: 10,
            label: 'nnnnnnnnn'
        }
    ]

    let setW = 450
    let svg = d3.select('div')
        .append('svg')
        .attr('width', setW)
        .attr('height', 300)

    let rect = svg.append('rect')
        .attr('width', setW)
        .attr('height', 300)
        .attr('fill', 'black')

    let textG = svg.selectAll('text')
        .data(data)
        .enter()
        .append('text')
        .attr('fill', '#fff')
        .text((d) => d.label)

    let arr = []
    let paddingLeft = 10

    defaultMode()

    /**
     * 普通换行模式
     **/
    function defaultMode() {
        arr = []
        let w = 0
        let x = 0
        let xCount = 0   // 累计每一行有几个元素
        let yCount = 1
        textG.attr('x', function (d, i) {
            // 超出设定父级宽度,x重置为0
            // 略过0,因为0的上一个兄弟元素是rect
            if (x + this.getBBox().width + this.previousElementSibling.getBBox().width + paddingLeft > rect.node().getBBox().width || i === 0) {
                if (w > 0) {
                    arr.push({
                        w: w,           // 收集每一行的宽度
                        count: xCount   // 累计会比实际元素个数少一个,因为换行后的第一个元素不需要计算间隔距离
                    })
                }
                w = this.getBBox().width
                x = 0
                xCount = 0
                return x
            } else {
                xCount++
                w += this.getBBox().width

                // 移动距离 = 上一个兄弟元素的x坐标 + 上一个兄弟元素的宽度 + 间隔
                x = this.previousElementSibling.getBBox().x + this.previousElementSibling.getBBox().width + paddingLeft
                return x
            }
        })   // 换行
            .attr('y', function (d, i) {
                // this.getBBox().x < 0 用于兼容chrome,chrome有时会出现-1的情况
                // 略过0,因为0的上一个兄弟元素是rect
                // this.getBBox().x <= 0 表示为换行后的第一个元素。直到碰到下一个this.getBBox().x <= 0才会加一
                if (this.getBBox().x <= 0 && i > 0) {
                    yCount++
                }
                return yCount * 21
            })

        // 最后一行少于设定长度不添加进数组
        if(w > rect.node().getBBox().width / 1.5){
            arr.push({
                w: w,           // 收集最后一行的宽度
                count: xCount   // 收集最后一行有几个元素
            })
        }
    }

    /**
     * 换行均匀分布模式
     **/
    function equallyMode() {
        let index = 0
        let space = 0
        if (arr.length) {
            textG.attr('x', function () {
                if (this.getBBox().x <= 0) {
                    // arr[index]为false间隔将保持为paddingLeft
                    space = paddingLeft

                    if (arr[index]) {
                        // rect宽度 - 文字所占宽度 = 空余的空间
                        // 空余的空间 / 元素个数 = 每个元素之间的间隔距离
                        space = (rect.node().getBBox().width - arr[index].w) / arr[index].count
                        index++
                    }
                    return 0
                } else {
                    // x轴移动距离 = 上一个兄弟元素的宽度 + 上一个兄弟元素的x坐标 + 间隔
                    return this.previousElementSibling.getBBox().width + this.previousElementSibling.getBBox().x + space
                }
            })
        }
    }
</script>
</body>
</html>