zoukankan      html  css  js  c++  java
  • d3.js 实现立体柱图

    前言

    随着大数据时代的来临,数据可视化的重要性也越来越凸显,那么今天就基于d3.js今天给大家带来可视化基础图表柱图进阶:立体柱图

    关于d3.js

    d3.js是一个操作svg的图表库,d3封装了图表的各种算法.对d3不熟悉的朋友可以到d3.js官网学习d3.js.
    另外感谢司机大傻(声音像张学友一样性感的一流装逼手)和司机呆(呆萌女神)等人对d3.js进行翻译!

    HTML+CSS

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>Title</title>
        <style>
            * {
                margin: 0;
                padding: 0;
            }
    
            div.tip-hill-div {
                background: rgba(0, 0, 0, 0.7);
                color: #fff;
                padding: 10px;
                border-radius: 5px;
                font-family: Microsoft Yahei;
            }
    
            div.tip-hill-div > h1 {
                font-size: 14px;
            }
    
            div.tip-hill-div > h2 {
                font-size: 12px;
            }
        </style>
    </head>
    <body>
    <div id="chart"></div>
    </body>
    </html>

    JS

    当前使用d3.v4+版本

    <script src="d3-4.js"></script>

    图表所需数据

    var data = [{
            "letter": "白皮鸡蛋",
            "child": {
                "category": "0",
                "value": "459.00"
            }
        }, {
            "letter": "红皮鸡蛋",
            "child": {
                "category": "0",
                "value": "389.00"
            }
        }, {
            "letter": "鸡蛋",
            "child": {
                "category": "0",
                "value": "336.00"
            }
        }, {
            "letter": "牛肉",
            "child": {
                "category": "0",
                "value": "282.00"
            }
        }, {
            "letter": "羊肉",
            "child": {
                "category": "0",
                "value": "249.00"
            }
        }, {
            "letter": "鸭蛋",
            "child": {
                "category": "0",
                "value": "242.00"
            }
        }, {
            "letter": "红薯",
            "child": {
                "category": "0",
                "value": "222.00"
            }
        }, {
            "letter": "白菜",
            "child": {
                "category": "0",
                "value": "182.00"
            }
        }, {
            "letter": "鸡肉",
            "child": {
                "category": "0",
                "value": "102.00"
            }
        }];

    图表的一些基础配置数据

    var margin = {
            top: 20,
            right: 50,
            bottom: 50,
            left: 90
        };
    
    var svgWidth = 1000;
    var svgHeight = 500;
    
    
    //创建各个面的颜色数组
    var mainColorList = ['#f6e242', '#ebec5b', '#d2ef5f', '#b1d894','#97d5ad', '#82d1c0', '#70cfd2', '#63c8ce', '#50bab8', '#38a99d'];
    var topColorList = ['#e9d748', '#d1d252', '#c0d75f', '#a2d37d','#83d09e', '#68ccb6', '#5bc8cb', '#59c0c6', '#3aadab', '#2da094'];
    var rightColorList = ['#dfce51', '#d9db59', '#b9d54a', '#9ece7c','#8ac69f', '#70c3b1', '#65c5c8', '#57bac0', '#42aba9', '#2c9b8f'];
    
    var svg = d3.select('#chart')
            .append('svg')
            .attr('width', svgWidth)
            .attr('height', svgHeight)
            .attr('id', 'svg-column');

    创建X轴序数比例尺

    function addXAxis() {
            var transform = d3.geoTransform({
                point: function (x, y) {
                    this.stream.point(x, y)
                }
            });
            //定义几何路径
            var path = d3.geoPath()
                    .projection(transform);
    
            xLinearScale = d3.scaleBand()
                    .domain(data.map(function (d) {
                        return d.letter;
                    }))
                    .range([0, svgWidth - margin.right - margin.left], 0.1);
            var xAxis = d3.axisBottom(xLinearScale)
                    .ticks(data.length);
            //绘制X轴
            var xAxisG = svg.append("g")
                    .call(xAxis)
                    .attr("transform", "translate(" + (margin.left) + "," + (svgHeight - margin.bottom) + ")");
    
            //删除原X轴
            xAxisG.select("path").remove();
            xAxisG.selectAll('line').remove();
            //绘制新的立体X轴
            xAxisG.append("path")
                    .datum({
                        type: "Polygon",
                        coordinates: [
                            [
                                [20, 0],
                                [0, 15],
                                [svgWidth - margin.right - margin.left, 15],
                                [svgWidth + 20 - margin.right - margin.left, 0],
                                [20, 0]
                            ]
                        ]
                    })
                    .attr("d", path)
                    .attr('fill', 'rgb(187,187,187)');
            xAxisG.selectAll('text')
                    .attr('font-size', '18px')
                    .attr('fill', '#646464')
                    .attr('transform', 'translate(0,20)');
    
            dataProcessing(xLinearScale)//核心算法
        }

    你可能注意到了,上面代码中不仅使用了序数比例尺,还有地理路径生成器,因为需要生成立体的柱图,所以需要讲原本的X轴删除,自己重新进行绘制.下图是自己重新绘制出来的path路径:

    创建Y轴线性比例尺

    var yLinearScale;
        //创建y轴的比例尺渲染y轴
        function addYScale() {
            yLinearScale = d3.scaleLinear()
                    .domain([0, d3.max(data, function (d, i) {
                        return d.child.value * 1;
                    }) * 1.2])
                    .range([svgHeight - margin.top - margin.bottom, 0]);
    
            //定义Y轴比例尺以及刻度
            var yAxis = d3.axisLeft(yLinearScale)
                    .ticks(6);
    
            //绘制Y轴
            var yAxisG = svg.append("g")
                    .call(yAxis)
                    .attr('transform', 'translate(' + (margin.left + 10) + "," + margin.top + ")");
            yAxisG.selectAll('text')
                    .attr('font-size', '18px')
                    .attr('fill', '#636363');
            //删除原Y轴路径和tick
            yAxisG.select("path").remove();
            yAxisG.selectAll('line').remove();
        }

    创建Y轴时同样需要把原来的路径和tick删除,下图是效果:


    到这,我们的基础搭建完毕,下面就是核心算法

    核心算法

    为了实现最终效果,我希望大家在理解的时候能把整个立体柱图分解一下.


    我实现立体柱图的思路是通过2个path路径和一个rect进行拼凑.
    正面是一个rect,上面和右面利用path路径生成.
    利用三角函数,通过给定的angle角度计算上面的一个点就可以知道其他所有点的位置进而进行绘制.

    通过上图可以看到,一个立体柱图我们只需要知道7个点的位置就能够绘制出来.
    并且已知正面rect4个红色点的位置.已知柱子的宽度和高度,那么只要求出Top面左上角点的位置,就可以知道余下绿色点的位置.具体算法如下:

    //核心算法思路是Big boss教的,我借花献佛
    function dataProcessing(xLinearScale) {
            var angle = Math.PI / 2.3;
            for (var i = 0; i < data.length; i++) {
                var d = data[i];
                var depth = 10; 
                d.ow = xLinearScale.bandwidth() * 0.7;
                d.ox = xLinearScale(d.letter);
                d.oh = 1;
                d.p1 = {
                    x: Math.cos(angle) * d.ow,
                    y: -Math.sin(angle) - depth
                };
                d.p2 = {
                    x: d.p1.x + d.ow,
                    y: d.p1.y
                };
                d.p3 = {
                    x: d.p2.x,
                    y: d.p2.y + d.oh
                };
            }
        }

    渲染

    最终我们还要鼠标进行交互,所以先添加tip生成函数

    //tip的创建方法(方法来自敬爱的鸣哥)
        var tipTimerConfig = {
            longer: 0,
            target: null,
            exist: false,
            winEvent: window.event,
            boxHeight: 398,
            boxWidth: 376,
            maxWidth: 376,
            maxHeight: 398,
            tooltip: null,
    
            showTime: 3500,
            hoverTime: 300,
            displayText: "",
            show: function (val, e) {
                "use strict";
                var me = this;
    
                if (e != null) {
                    me.winEvent = e;
                }
    
                me.displayText = val;
    
                me.calculateBoxAndShow();
    
                me.createTimer();
            },
            calculateBoxAndShow: function () {
                "use strict";
                var me = this;
                var _x = 0;
                var _y = 0;
                var _w = document.documentElement.scrollWidth;
                var _h = document.documentElement.scrollHeight;
                var wScrollX = window.scrollX || document.body.scrollLeft;
                var wScrollY = window.scrollY || document.body.scrollTop;
                var xMouse = me.winEvent.x + wScrollX;
                if (_w - xMouse < me.boxWidth) {
                    _x = xMouse - me.boxWidth - 10;
                } else {
                    _x = xMouse;
                }
    
                var _yMouse = me.winEvent.y + wScrollY;
                if (_h - _yMouse < me.boxHeight + 18) {
                    _y = _yMouse - me.boxHeight - 25;
                } else {
    
                    _y = _yMouse + 18;
                }
    
                me.addTooltip(_x, _y);
            },
            addTooltip: function (page_x, page_y) {
                "use strict";
                var me = this;
    
                me.tooltip = document.createElement("div");
                me.tooltip.style.left = page_x + "px";
                me.tooltip.style.top = page_y + "px";
                me.tooltip.style.position = "absolute";
    
                me.tooltip.style.width = me.boxWidth + "px";
                me.tooltip.style.height = me.boxHeight + "px";
                me.tooltip.className = "three-tooltip";
    
                var divInnerHeader = me.createInner();
                divInnerHeader.innerHTML = me.displayText;
                me.tooltip.appendChild(divInnerHeader);
    
                document.body.appendChild(me.tooltip);
            },
            createInner: function () {
                "use strict";
                var me = this;
                var divInnerHeader = document.createElement('div');
                divInnerHeader.style.width = me.boxWidth + "px";
                divInnerHeader.style.height = me.boxHeight + "px";
                return divInnerHeader;
            },
            ClearDiv: function () {
                "use strict";
                var delDiv = document.body.getElementsByClassName("three-tooltip");
                for (var i = delDiv.length - 1; i >= 0; i--) {
                    document.body.removeChild(delDiv[i]);
                }
            },
            createTimer: function (delTarget) {
                "use strict";
                var me = this;
                var delTip = me.tooltip;
                var delTarget = tipTimerConfig.target;
                var removeTimer = window.setTimeout(function () {
                    try {
                        if (delTip != null) {
                            document.body.removeChild(delTip);
                            if (tipTimerConfig.target == delTarget) {
                                me.exist = false;
                            }
                        }
                        clearTimeout(removeTimer);
                    } catch (e) {
                        clearTimeout(removeTimer);
                    }
                }, me.showTime);
            },
            hoverTimerFn: function (showTip, showTarget) {
                "use strict";
                var me = this;
    
                var showTarget = tipTimerConfig.target;
    
                var hoverTimer = window.setInterval(function () {
                    try {
                        if (tipTimerConfig.target != showTarget) {
                            clearInterval(hoverTimer);
                        } else if (!tipTimerConfig.exist && (new Date()).getTime() - me.longer > me.hoverTime) {
                            //show
                            tipTimerConfig.show(showTip);
                            tipTimerConfig.exist = true;
                            clearInterval(hoverTimer);
                        }
                    } catch (e) {
                        clearInterval(hoverTimer);
                    }
                }, tipTimerConfig.hoverTime);
            }
        };
    
        var createTooltipTableData = function (info) {
            var ary = [];
            ary.push("<div class='tip-hill-div'>");
            ary.push("<h1>品种信息:" + info.letter + "</h1>");
            ary.push("<h2>成交量: " + info.child.value);
            ary.push("</div>");
            return ary.join("");
        };

    核心算法写完,就到了最终的渲染了

    function addColumn() {
            function clumnMouseover(d) {
                d3.select(this).selectAll(".transparentPath").attr("opacity", 0.8);
                // 添加 div
                tipTimerConfig.target = this;
                tipTimerConfig.longer = new Date().getTime();
                tipTimerConfig.exist = false;
                //获取坐标
                tipTimerConfig.winEvent = {
                    x: event.clientX - 100,
                    y: event.clientY
                };
                tipTimerConfig.boxHeight = 50;
                tipTimerConfig.boxWidth = 140;
    
                //hide
                tipTimerConfig.ClearDiv();
                //show
                tipTimerConfig.hoverTimerFn(createTooltipTableData(d));
            }
    
            function clumnMouseout(d) {
                d3.select(this).selectAll(".transparentPath").attr("opacity", 1);
                tipTimerConfig.target = null;
                tipTimerConfig.ClearDiv();
            }
    
            var g = svg.selectAll('.g')
                    .data(data)
                    .enter()
                    .append('g')
                    .on("mouseover", clumnMouseover)
                    .on("mouseout", clumnMouseout)
                    .attr('transform', function (d) {
                        return "translate(" + (d.ox + margin.left + 20) + "," + (svgHeight - margin.bottom + 15) + ")"
                    });
            g.transition()
                    .duration(2500)
                    .attr("transform", function (d) {
                        return "translate(" + (d.ox + margin.left + 20) + ", " + (yLinearScale(d.child.value) + margin.bottom - 15) + ")"
                    });
    
            g.append('rect')
                    .attr('x', 0)
                    .attr('y', 0)
                    .attr("class", "transparentPath")
                    .attr('width', function (d, i) {
                        return d.ow;
                    })
                    .attr('height', function (d) {
                        return d.oh;
                    })
                    .style('fill', function (d, i) {
                        return mainColorList[i]
                    })
                    .transition()
                    .duration(2500)
                    .attr("height", function (d, i) {
                        return svgHeight - margin.bottom - margin.top - yLinearScale(d.child.value);
                    });
    
            g.append('path')
                    .attr("class", "transparentPath")
                    .attr('d', function (d) {
                        return "M0,0 L" + d.p1.x + "," + d.p1.y + " L" + d.p2.x + "," + d.p2.y + " L" + d.ow + ",0 L0,0";
                    })
                    .style('fill', function (d, i) {
                        return topColorList[i]
                    });
    
            g.append('path')
                    .attr("class", "transparentPath")
                    .attr('d', function (d) {
                        return "M" + d.ow + ",0 L" + d.p2.x + "," + d.p2.y + " L" + d.p3.x + "," + d.p3.y + " L" + d.ow + "," + d.oh + " L" + d.ow + ",0"
                    })
                    .style('fill', function (d, i) {
                        return rightColorList[i]
                    })
                    .transition()
                    .duration(2500)
                    .attr("d", function (d, i) {
                        return "M" + d.ow + ",0 L" + d.p2.x + "," + d.p2.y + " L" + d.p3.x + "," + (d.p3.y + svgHeight - margin.top - margin.bottom - yLinearScale(d.child.value)) + " L" + d.ow + "," + (svgHeight - margin.top - margin.bottom - yLinearScale(d.child.value)) + " L" + d.ow + ",0"
                    });
        }

    由于需要考虑动画,所以对渲染时的柱子位置进行了处理.对这方面不理解的话可以留言讨论.

    如需转载请注明出处!

  • 相关阅读:
    TabControl添加关闭按钮
    Windows & RabbitMQ:集群(clustering) & 高可用(HA)
    Windows & RabbitMQ:Shovel
    15项最佳电子产品影响人类未来
    收藏很久的开关电源书籍
    我也不想这样(转载)
    vbs 脚本轻松搞定JDK的环境变量配置
    开关电源基本知识
    浅谈软件开发定律系列之帕金森定律(Parkinson’s Law)
    堕落的时候看看——清华大学老师的一席话
  • 原文地址:https://www.cnblogs.com/libin-1/p/6778595.html
Copyright © 2011-2022 走看看