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"
                });
    }

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

 

如需转载请注明出处!

posted @ 2017-04-28 00:59  最骚的就是你  阅读(5808)  评论(0编辑  收藏  举报