d3js 带阴影的环形图、饼图,图例动态居中显示

1,276 阅读3分钟

效果预览

16825590-54d7e8bf2caa3741.png 16825590-eaee536e4ac1a9ec.png

源码

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <style>
        body {
            background: black;
            width: 400px;
            height: 400px;
        }

        .arc path {
            cursor: pointer;
        }

        .tips {
            display: none;
            position: absolute;
            background: #ffffff;
            border: 1px #6a5acd solid;
            border-radius: 5px;
            padding: 10px;
        }

        .tips div {
            margin: 5px 0 5px 0;
        }

        .tips div i {
            display: inline-block;
            width: 12px;
            height: 12px;
            background-color: slateblue;
            border-radius: 50%;
            margin-right: 5px;
        }

        .tips div span:nth-child(3) {
            margin-left: 30px;
        }

        .middle-title, .middle-number {
            text-anchor: middle;
            fill: #ffffff;
        }

        .middle-title {
            dominant-baseline: middle;
        }

        .rect {
            cursor: pointer;
        }
    </style>
</head>
<body>
<!--提示框-->
<div class="tips">
    <div>来源</div>
    <div>
        <i></i>
        <span class="title"></span>
        <span class="number"></span>
    </div>
</div>
<svg viewBox="0 0 400 400" height="100%" width="100%" preserveAspectRatio="xMinYMin meet"
     xmlns="http://www.w3.org/2000/svg" version="1.1">
    <!--    阴影-->
    <defs>
        <filter id="shadow" x="-2" y="-2" width="500%" height="500%">
            <feOffset result="offOut" in="SourceGraphic" dx="0" dy="0"/>
            <feGaussianBlur result="blurOut" in="offOut" stdDeviation="10"/>
            <feBlend in="SourceGraphic" in2="blurOut" mode="normal"/>
        </filter>
    </defs>
</svg>
<script src="d3.v6.js"></script>
<script>
    let hsl = null
    let w = 400
    let h = 400

    let legendPadding = 30;     // 图例间隔距离
    let outerRadius = w / 2
    let innerRadius = w / 3
    let x = outerRadius - 45;   // 图形移动位置
    let dataset = [{
        value: 5,
        title: 'bhjb'
    }, {
        value: 10,
        title: '123'
    }, {
        value: 20,
        title: '000'
    }, {
        value: 45,
        title: '是的发'
    }, {
        value: 20,
        title: '898'
    }, {
        value: 25,
        title: '5656'
    }]
    let pie = d3.pie().value(function (d) {
        return d.value
    }).sortValues(function (a, b) {
        return a
    })
    let radiusSize = [15, 65]
    let arc = arcFunc(innerRadius - radiusSize[0], outerRadius - radiusSize[1]);

    function arcFunc(innerRadius, outerRadius) {
        return d3.arc().innerRadius(innerRadius).outerRadius(outerRadius).cornerRadius(10).padAngle(0.05);
    }

    let svg = d3.select('svg')


    svg._groups[0][0].onload = function () {
        let percentage = x / this.clientWidth * 100
        // 渐变色
        svg._groups[0][0].innerHTML +=
            `<defs>
        <radialGradient id="showdown" cx="${percentage}%" cy="50%" r="25%" fx="${percentage}%" fy="50%" gradientUnits="userSpaceOnUse">
            <stop offset="0%" stop-color="rgba(255,255,255,1)"/>
            <stop offset="50%" stop-color="rgba(255,255,255,.5)"/>
            <stop offset="100%" stop-color="rgba(255,255,255,0)"/>
        </radialGradient>
    </defs>`

        // 中间数字
        svg.append('text')
            .classed('middle-number', true)
            .text(d3.sum(dataset, function (d) {
                return d.value
            }))
            .attr('font-size', '40')
            .attr('x', x)
            .attr('y', outerRadius - 30)
        // 中间文字
        svg.append('text')
            .classed('middle-title', true)
            .text('总数')
            .attr('font-size', '40')
            .attr('x', x)
            .attr('y', outerRadius + 60)

        let arcs = svg.selectAll('g.arc')
            .data(pie(dataset))
            .enter()
            .append('g')
            .classed('arc', true)
            .attr('transform', `translate(${x},${outerRadius})`)

        arcs.append('path')
            .attr('fill', (d, i) => {
                return d3.schemeCategory10[i]
            })
            .attr('d', arc)
            .attr('filter', 'url(#shadow)')
            // 鼠标移入
            .on('mousemove', function (d, value) {
                // 显示提示框
                tipsShow(d, value)
                // 样式改变
                pathStyle.call(this, value, true)
            })
            // 鼠标移出
            .on('mouseout', function (d, value) {
                // 隐藏提示框
                tipsHide()
                // 还原样式
                pathStyle.call(this, value, false)
            });

        // 图例
        let g = svg.append('g')

        let g1 = g.selectAll('g.rect')
            .data(dataset, function (d, i) {
                d.index = i
                return i
            })
            .enter()
            .append('g')
            .attr('class', 'rect')
            .on('mousemove', function (d, val) {
                tipsShow(d, val, false)
                pathStyle.call(arcs._groups[0][val.index].children[0], val, true)
            })
            .on('mouseout', function (d, val) {
                tipsHide()
                pathStyle.call(arcs._groups[0][val.index].children[0], val, false)
            })

        let text = g1.append('text')
            .text(function (d) {
                return d.title
            })
            .attr('y', '16')
            .attr('fill', '#ffffff')
            .attr('x', '25')

        // 图例文字长度
        let numArr = [];
        for (let item of text._groups[0]) {
            numArr.push(item.getBBox().width)
        }
        // 获取最长的文字长度
        let maxNum = d3.max(numArr)

        g1.append('rect')
            .attr('width', 20)
            .attr('height', 20)
            .attr('fill', function (d, i) {
                return d3.schemeCategory10[i]
            })
            .attr('rx', '5')
            .attr('ry', '5')

        // 中间横线
        svg.append('line')
            .attr('x1', x - 100)
            .attr('y1', outerRadius)
            .attr('x2', x + 100)
            .attr('y2', outerRadius)
            .attr('stroke', 'url(#showdown)')
            .attr('stroke-width', '2')

        legendTranslate('right')

        /**
         * 图例排序及居中计算
         * @param {string} key - 图例显示的位置
         **/
        function legendTranslate(key) {
            let num = 0
            if (key === 'top') {
                // 图例顶部
                g1.attr('transform', function (d, i) {
                    if (i) {
                        num += (text._groups[0][i === 1 ? 0 : i - 1].getBBox().width) + legendPadding
                    }
                    return `translate(${num},0)`
                })
            }
            if (key === 'right') {
                // 图例右边排列
                g1.attr('transform', function (d, i) {
                    if (i) {
                        num += legendPadding  // 各图例间隔
                    }
                    return `translate(0,${num})`
                })
            }
            // 居中计算
            let n = outerRadius - (g.node().getBBox()[key === 'top' ? 'width' : 'height'] / 2)

            g.attr('transform', `translate(${key === 'top' ? n : w - (maxNum + legendPadding)},${key === 'right' ? n : 0})`)
        }

        /**
         * 显示提示框/中间内容改变
         * @param {HTMLElement}    d - dom元素
         * @param {Object}       val - 饼图数据
         * @param {Boolean} tipsHide - 是否展示提示框
         **/
        function tipsShow(d, val, tipsHide = true) {
            let title = val.data ? val.data.title : val.title
            let value = val.data ? val.data.value : val.value
            // 提示框
            d3.selectAll('.tips div i')
                .style('background', d3.schemeCategory10[val.index])

            // 标题
            d3.select('.tips .title')
                .text(title)

            // 百分比
            d3.select('.tips .number')
                .text(function () {
                    let percent = Number(value) / d3.sum(dataset, function (d) {
                        return d.value;
                    }) * 100
                    return `${percent.toFixed(1)}%`
                })

            d3.select('.tips')
                .style('display', tipsHide ? 'block' : 'none')
                .style('left', `${d.clientX + 20}px`)
                .style('top', `${d.clientY}px`)
                .style('border-color', d3.schemeCategory10[val.index])

            // 环形图中间内容
            d3.select('.middle-title')
                .text(title)

            d3.select('.middle-number')
                .text(value)
        }

        /**
         * 隐藏提示框/中间内容还原
         **/
        function tipsHide() {
            // 提示框
            d3.select('.tips')
                .style('display', 'none')

            // 环形图中间内容
            d3.select('.middle-title')
                .text('总数')

            d3.select('.middle-number')
                .text(d3.sum(dataset, function (d) {
                    return d.value
                }))
        }

        /**
         * 当前path样式改变
         * @param {Object} value - 饼图数据
         * @param {Boolean} into - true改变样式,false还原
         **/
        function pathStyle(value, into = false) {
            if (into) {
                hsl = d3.hsl(d3.schemeCategory10[value.index])
                hsl.s += 0.1
                d3.select(this)
                    .transition()
                    .duration(400)
                    .attr('fill', hsl)
                    .attr('d', arcFunc(innerRadius - radiusSize[0], outerRadius - (radiusSize[1] - 5)))
            } else {
                hsl = d3.hsl(d3.schemeCategory10[value.index])
                hsl.s -= 0.1
                d3.select(this)
                    .transition()
                    .duration(400)
                    .attr('fill', hsl)
                    .attr('d', arc)
            }
        }
    }
</script>
</body>
</html>