D3.js画图:3D动态饼图(齿轮图)

介绍

通常画可视化图的工具很多,除了d3.js,还有echarts.js等。

d3.js: 国外工程师做的,英文文档,不太好理解,本质是svg。例子丰富,但是没有细致的分类,所以找起来比较费劲。
https://github.com/d3/d3/wiki/Gallery

ECharts.js: 百度做的,中文文档,容易理解,基于HTML5的图形库,本质是canvas。例子也比较丰富,方便参考和开发。
https://echarts.baidu.com/echarts2/doc/example.html

通过比较,看起来ECharts.js更容易上手,但是因为我需要更灵活更符合个性定制化的工具,所以选了d3.js。

3D饼图(齿轮图)

经过一段时间的磨炼,从折线图、闭合路径图、蜂窝图、直角坐标、极坐标都玩了个遍。
那这次就来个3D的吧,其实d3.js做3D的图不是很容易的,有更好的选择,但我认准了d3.js,一条道走到黑吧(想起高中数学老师说的话,当你解题解到一半时发现有更好的办法,不,赶紧忘掉,接着当前的方法,只要方法没错,总能解出来,也许会傻一点,但是一定会有正确的结果;如果中途放弃,也许另一个方法更快更聪明,但也许更慢或者错误,不算到最后,谁都不知道谁最准确。我选择相信他的话,于是。。。我成了程序员O(∩_∩)O哈哈~)。

站在巨人肩上

有人鄙视拿来主义,要我说,你能拿来那是你的本事,如果还能在此基础上做出更好的东西,何乐而不为呢?
每个人时间有限,每个项目也有deadline,不可能从每一个螺丝钉怎么拧开始学起,不然怎么会有那么多五花八门的框架,会有封装好的组件和接口,正因为有人已经做了前期工作,所以时间才能省下来做更有意义的事情,这就是站在巨人肩上的道理所在吧。
但是我们得明白拿来的东西的原理,以及出了问题该怎么解决的能力。然后才能做出更厉害的东西。

谁是巨人

首选当然是官网的例子咯,目测搜了一圈,终于找到一个3D Donut。就是你了,我的巨人。
把该地址的donut3d.js拷贝下来作为画3D饼图的基础js,待会会在此基础上修改,以满足我的要求(长的像齿轮的要求)。

http://bl.ocks.org/NPashaP/9994181

那我们就一睹她的芳容吧。
如果这张图符合你的要求,那就打住,不用往下看了,直接看官网例子即可。
注意d3版本的问题,如果你用d3.v3.js,恭喜你,啥也不用改,直接拿来用;如果你用d3.v5.js,那稍微改下方法,比如d3.v5.js没有d3.layout,所以d3.layout.pie改成d3.pie。我就是那个不幸的人,用的d3.v5.js。没关系,改起来很快,运行下,看哪里有错,就改哪里,O(∩_∩)O哈哈~so easy!

D3.js画图:3D动态饼图(齿轮图)_第1张图片
donut3d.png

如何添加自己的需求

还是先上个我已经改好之后的3D饼图(齿轮图)吧,方便说明。
其实显示的时候是个动态的,一节一节显示出齿轮的。
背景是黑乎乎的,据说现在流行黑乎乎的背景,显得有科技感,技术也要赶时髦啊,我这么fashion的人,做出来的东西也要fashion啊O(∩_∩)O~

D3.js画图:3D动态饼图(齿轮图)_第2张图片
donut3d-sq.png

分析:

  1. 显示百分比文字。
  2. 有好几个椭圆,空心椭圆和实心椭圆。
  3. 齿轮中间有缝隙,怎么实现这个缝隙呢?很容易想到的,透明嘛,transparent。
  4. 中间有了缝隙之后,那齿轮还完整吗?当中间没有缝隙时,看官网的原图,仔细看,发现其实只实现了3个面:上面、外面和里面;而有了缝隙后,看我做的图,一块立体小齿轮有6个面,想象下魔方,是不是6个面?那从实现和视觉效果上来说,不需要6个面完整实现,但是至少需要实现5个面:上面、外面、里面、左侧面和右侧面。

从以上分析可以看出,难啃的骨头在第4点。这个图断断续续花了3天时间才搞定,为啥是断断续续呢,因为还有其他工作要做嘛,你懂得。
那就按顺序一条一条实现,总有一天我们的愿望都能实现!

svg

首先新建svg及设置宽高。

  var donutChart = document.getElementById(self.id);
  if (!donutChart)
    return

  var width = donutChart.clientWidth;
  var height = donutChart.clientHeight;
  var pdata = 80; // 显示80%

  donutChart.innerHTML = ""
  var svg = d3.select("#" + self.id).append("svg")
    .attr("width", width)
    .attr("height", height)
    .append("g");

显示百分比文字

  // 画百分比
  svg.append("text")
    .attr("x", width / 2 - 20) //文本的起始x坐标
    .attr("y", height / 6) //文本的起始x坐标
    .attr("class", "percent_text")
    .text(pdata + "%");

画椭圆

  // 画椭圆
  svg.append("ellipse")
    .attr("cx", 0)
    .attr("cy", 0)
    .attr("rx", width / 2 - 20)
    .attr("ry", height / 4)
    .attr("class", "circle_line")
    .attr("transform", "translate(" + width / 2 + "," + height * 1.2 / 2 + ")");
  svg.append("ellipse")
    .attr("cx", 0)
    .attr("cy", 0)
    .attr("rx", width / 8)
    .attr("ry", height / 13)
    .attr("class", "circle_line")
    .attr("transform", "translate(" + width / 2 + "," + (height / 2 + 5) + ")");
  svg.append("ellipse")
    .attr("cx", 0)
    .attr("cy", 0)
    .attr("rx", width / 15)
    .attr("ry", height / 22)
    .attr("class", "circle_line")
    .attr("transform", "translate(" + width / 2 + "," + (height / 2 + 5) + ")");
  svg.append("ellipse")
    .attr("cx", 0)
    .attr("cy", 0)
    .attr("rx", width / 25)
    .attr("ry", height / 40)
    .attr("fill", "#2D3C41") // 实心椭圆:填充颜色
    .attr("transform", "translate(" + width / 2 + "," + (height / 2 + 5) + ")");

切分成32个小齿轮

我是切分成了32个小齿轮(包含透明的),如果你想分的更细,可以分成40或50个,只要你觉得好看就行。
既然要分成32个小立体快,那数据也要切分成32个。

不算透明的,那就是16块小齿轮。
暗颜色的小齿轮是底色,亮颜色小齿轮是百分比。
如果显示80%,经过计算(80/100)*16=12.8,四舍五入为13,所以有13个亮颜色的小齿轮。

  var baseColor = "#2D3C41" // 暗颜色
  var color = "#5D858D" // 亮颜色
  var colorSize = Math.round(16.0*pdata/100.0) // 连颜色数量
  var dataset = []
  var index = 1;

  while(index < colorSize*2) {
    if (index % 2 == 0)
      dataset.push({label: index.toString(), value: 3.125, color: "transparent"}) // 缝隙
    else
      dataset.push({label: index.toString(), value: 3.125, color: color}) // 亮颜色
    index ++
  }

  while(index <= 32) {
    if (index % 2 == 0)
      dataset.push({label: index.toString(), value: 3.125, color: "transparent"}) // 缝隙
    else
      dataset.push({label: index.toString(), value: 3.125, color: baseColor}) // 暗颜色
    index ++
  }

通过以上处理,把数据整合成可以生成齿轮的完整数据dataset。

增加左侧面和右侧面

如果不增加左侧面和右侧面,那调用donut3d.js的draw方法后,会生成什么样的图形呢?

// 画3D饼图
svg.append("g").attr("id", "salesDonut");
Donut3D.draw("salesDonut", dataset, width / 2, height / 2, width / 2 - 30, height / 4, 10, 0.6);

请各位仔细看。


D3.js画图:3D动态饼图(齿轮图)_第3张图片
donut3d-sq1.png

是不是有种被掏空的感觉?如果你觉得这样挺好看,那也行,打住吧,后面就不用再看了;如果你想补齐其他面,请耐心往下看。
经过观察和比较,增加左侧面和右侧面就能填满空虚的心啦啦啦~
这次要在donut3d.js这个巨人身上添砖加瓦咯。

// 左侧面
function pieLeftSide(d, rx, ry, h, ir) {
  var startAngle = d.startAngle;
  var endAngle = d.endAngle;

  var sx = rx * Math.cos(startAngle),
  sy = ry * Math.sin(startAngle),
  ex = rx * Math.cos(endAngle),
  ey = ry * Math.sin(endAngle);

  var ret = [];
  ret.push("M", ir * ex, h + ir * ey, "L", ir * ex, ir * ey, "L", ex, ey, "L", ex, h + ey, "z");
  return ret.join(" ");
}

// 右侧面
function pieRightSide(d, rx, ry, h, ir) {
  var startAngle = d.startAngle;
  var endAngle = d.endAngle;

  var sx = rx * Math.cos(startAngle),
  sy = ry * Math.sin(startAngle),
  ex = rx * Math.cos(endAngle),
  ey = ry * Math.sin(endAngle);

  var ret = [];
  ret.push("M", sx, h + sy, "L", sx, sy, "L", ir * sx, ir * sy, "L", ir * sx, h + ir * sy, "z");
  return ret.join(" ");
}

然后再用新增加的两个方法画出左右侧面。

slices.selectAll(".rightSideSlice").data(_data).enter().append("path").attr("class", "rightSideSlice")
  .style("fill", function (d) {
    if (d.data.color == "transparent")
      return "transparent";
    else
      return d3.hsl(d.data.color).darker(1.5);
  })
  .attr("d", function (d) { return pieRightSide(d, rx, ry, h, ir); })
  .each(function (d) { this._current = d; })
  .attr("opacity", 0)
  .transition()
  .delay(function (d, i) { // 延时显示,最终效果是一节一节动态显示
    return i * 50;
  })
  .duration(100)
  .attr("opacity", 1);

slices.selectAll(".leftSideSlice").data(_data).enter().append("path").attr("class", "leftSideSlice")
  .style("fill", function (d) {
    if (d.data.color == "transparent")
      return "transparent";
    else
      return d3.hsl(d.data.color).darker(1.5);
  })
  .attr("d", function (d) { return pieLeftSide(d, rx, ry, h, ir); })
  .each(function (d) { this._current = d; })
  .attr("opacity", 0)
  .transition()
  .delay(function (d, i) { // 延时显示,最终效果是一节一节动态显示
    return i * 50;
  })
  .duration(100)
  .attr("opacity", 1);

最终成品

终于填满需要的每一面,看上去像个立体齿轮图了。
这个图是很久之前做的,当时花了很长时间调试,每一个面有4条边,定位2个点,再加上高度和内半径,就可以计算出4个点,然后就可以画出4条边,最后填充颜色,一个面就完成了。
最近整理文档时觉得有必要写出来,方便以后查阅和探讨,也告诉自己积累是一个长期过程,不急不躁,慢慢来,一步一步完成既定目标,总有一天你会走遍技术的每个角落。
现在我整理成vue组件,传一个百分比的参数,就可以显示3D齿轮图了,我的3D齿轮图也成巨人啦。

D3.js画图:3D动态饼图(齿轮图)_第4张图片
donut3d-sq.png

你可能感兴趣的:(D3.js画图:3D动态饼图(齿轮图))