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" }); }
由于需要考虑动画,所以对渲染时的柱子位置进行了处理.对这方面不理解的话可以留言讨论.
总结
以上就是这篇文章的全部内容了,希望本文的内容对大家学习或者工作能带来一定的帮助,如果有疑问大家可以留言交流,谢谢大家对的支持。