如何使用d3.js制作可视化图表

JavaScript019

如何使用d3.js制作可视化图表,第1张

D3是目前最流行的JavaScript可视化图表库之一,D3的图表类型非常丰富,并且支持SVG格式,因此应用十分广泛,也有很多图表插件基于D3开发,比如MetricsGraphics.js,在D3上构建的数据图表非常强大。

D3的特点

允许绑定任意数据到DOM,将数据驱动转换应用到Document中。

不仅可以创建精美的HTML表格,而且可以绘制折线图、柱形图和饼图等数据图表。

支持SVG,在Web页面上渲染毫无压力。

回到顶部

D3的使用方法

关于D3的具体用法,可以看D3图形库API参考这篇文章。本文主要对介绍一些经典图表的实现效果及代码。

index.html代码:

<!DOCTYPE html>

<meta charset="utf-8">

<style>

 

svg {

  font: 10px sans-serif

}

 

.y.axis path {

  display: none

}

 

.y.axis line {

  stroke: #fff

  stroke-opacity: .2

  shape-rendering: crispEdges

}

 

.y.axis .zero line {

  stroke: #000

  stroke-opacity: 1

}

 

.title {

  font: 300 78px Helvetica Neue

  fill: #666

}

 

.birthyear,

.age {

  text-anchor: middle

}

 

.birthyear {

  fill: #fff

}

 

rect {

  fill-opacity: .6

  fill: #e377c2

}

 

rect:first-child {

  fill: #1f77b4

}

 

</style>

<body>

<script src="http://d3js.org/d3.v3.min.js"></script>

<script>

 

var margin = {top: 20, right: 40, bottom: 30, left: 20},

    width = 960 - margin.left - margin.right,

    height = 500 - margin.top - margin.bottom,

    barWidth = Math.floor(width / 19) - 1

 

var x = d3.scale.linear()

    .range([barWidth / 2, width - barWidth / 2])

 

var y = d3.scale.linear()

    .range([height, 0])

 

var yAxis = d3.svg.axis()

    .scale(y)

    .orient("right")

    .tickSize(-width)

    .tickFormat(function(d) { return Math.round(d / 1e6) + "M" })

 

// An SVG element with a bottom-right origin.

var svg = d3.select("body").append("svg")

    .attr("width", width + margin.left + margin.right)

    .attr("height", height + margin.top + margin.bottom)

  .append("g")

    .attr("transform", "translate(" + margin.left + "," + margin.top + ")")

 

// A sliding container to hold the bars by birthyear.

var birthyears = svg.append("g")

    .attr("class", "birthyears")

 

// A label for the current year.

var title = svg.append("text")

    .attr("class", "title")

    .attr("dy", ".71em")

    .text(2000)

 

d3.csv("population.csv", function(error, data) {

 

  // Convert strings to numbers.

  data.forEach(function(d) {

    d.people = +d.people

    d.year = +d.year

    d.age = +d.age

  })

 

  // Compute the extent of the data set in age and years.

  var age1 = d3.max(data, function(d) { return d.age }),

      year0 = d3.min(data, function(d) { return d.year }),

      year1 = d3.max(data, function(d) { return d.year }),

      year = year1

 

  // Update the scale domains.

  x.domain([year1 - age1, year1])

  y.domain([0, d3.max(data, function(d) { return d.people })])

 

  // Produce a map from year and birthyear to [male, female].

  data = d3.nest()

      .key(function(d) { return d.year })

      .key(function(d) { return d.year - d.age })

      .rollup(function(v) { return v.map(function(d) { return d.people }) })

      .map(data)

 

  // Add an axis to show the population values.

  svg.append("g")

      .attr("class", "y axis")

      .attr("transform", "translate(" + width + ",0)")

      .call(yAxis)

    .selectAll("g")

    .filter(function(value) { return !value })

      .classed("zero", true)

 

  // Add labeled rects for each birthyear (so that no enter or exit is required).

  var birthyear = birthyears.selectAll(".birthyear")

      .data(d3.range(year0 - age1, year1 + 1, 5))

    .enter().append("g")

      .attr("class", "birthyear")

      .attr("transform", function(birthyear) { return "translate(" + x(birthyear) + ",0)" })

 

  birthyear.selectAll("rect")

      .data(function(birthyear) { return data[year][birthyear] || [0, 0] })

    .enter().append("rect")

      .attr("x", -barWidth / 2)

      .attr("width", barWidth)

      .attr("y", y)

      .attr("height", function(value) { return height - y(value) })

 

  // Add labels to show birthyear.

  birthyear.append("text")

      .attr("y", height - 4)

      .text(function(birthyear) { return birthyear })

 

  // Add labels to show age (separate not animated).

  svg.selectAll(".age")

      .data(d3.range(0, age1 + 1, 5))

    .enter().append("text")

      .attr("class", "age")

      .attr("x", function(age) { return x(year - age) })

      .attr("y", height + 4)

      .attr("dy", ".71em")

      .text(function(age) { return age })

 

  // Allow the arrow keys to change the displayed year.

  window.focus()

  d3.select(window).on("keydown", function() {

    switch (d3.event.keyCode) {

      case 37: year = Math.max(year0, year - 10) break

      case 39: year = Math.min(year1, year + 10) break

    }

    update()

  })

 

  function update() {

    if (!(year in data)) return

    title.text(year)

 

    birthyears.transition()

        .duration(750)

        .attr("transform", "translate(" + (x(year1) - x(year)) + ",0)")

 

    birthyear.selectAll("rect")

        .data(function(birthyear) { return data[year][birthyear] || [0, 0] })

      .transition()

        .duration(750)

        .attr("y", y)

        .attr("height", function(value) { return height - y(value) })

  }

})

1、模拟数据

// 模拟100条0-100的随机数,作为柱状图的高度

var data = Array.apply(0, Array(100)).map(function() {

 return Math.random() * 100

})

2、创建SVG容器

var margin = {top: 20, right: 20, bottom: 30, left: 50},

   width = document.body.clientWidth - margin.left - margin.right,

   height = 500 - margin.top - margin.bottom

var chart = d3.select('body')

 .append('svg')

 .attr('width', width + margin.left + margin.right)

 .attr('height', height + margin.top + margin.bottom)

 .append('g')

 .attr('transform', 'translate(' + margin.left + ', ' + margin.top + ')')

chart就是最终建立的容器,下面就往容器里面放元素。

3、画柱状图

// 计算每根柱状物体的宽度

var barWidth = width / data.length

// 用g作每根柱状物体的容器,意义可类比div

// 前一篇文章已经介绍过selectAll的意义,即生成占位符,等待填充svg图形

var bar = chart.selectAll('g')

 .data(data)

 .enter()

 .append('g')

 // 接收一个数据填充一个g元素

 // 同时为g设置位置

 .attr('transform', function(d, i) {

   return 'translate(' + i * barWidth + ', 0)'

 })

bar.append('rect')

 // 添加一个矩形

 .attr('y', function(d) {

   return height - d

 })

 .attr('height', function(d) {

   return d

 })

 .attr('width', barWidth - 1)

前文提到svg的元素定位都是基于整个svg容器左上角作为原点,但并不能使用position: absolute等方法定位,此处的g元素通过位移来定位x坐标,即transform: translate(x, 0)。

这里的bar可类比jQuery对象,是一个类数组对象,bar调用的方法都会对bar里面每个对象进行调用。代码中每一次调用都插入一个矩形,同时设置y坐标、高度和宽度,x坐标跟父容器(g)保持一致即可。这里需要注意y坐标往下为正,为了让所有矩形的下边处于同一高度,这里设置每个矩形的y坐标为容器高度减去矩形高度。为了用一像素区分开每个矩形,这里设置矩形宽度为父容器的宽度减1。

通过以上js代码再稍微设置一点css

rect {

 fill: #2177BB

}

即可看到一张最简单的柱状图了。

4、添加坐标轴

var y = d3.scale.linear()

 .domain([0, d3.max(data)])

 .range([height, 0])

var xAxis = d3.svg.axis()

 .scale(x)

 .orient('bottom')

 .ticks(1)

var yAxis = d3.svg.axis()

 .scale(y)

 .orient('left')

// 添加x坐标轴

chart.append('g')

 .attr('class', 'x axis')

 .attr('transform', 'translate(0,' + height + ')')

 .call(xAxis)

// 添加y坐标轴

chart.append('g')

 .attr('class', 'y axis')

 .call(yAxis)

完整的柱状图就是这样了

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

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

经过一段时间的磨炼,从折线图、闭合路径图、蜂窝图、直角坐标、极坐标都玩了个遍。

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

有人鄙视拿来主义,要我说,你能拿来那是你的本事,如果还能在此基础上做出更好的东西,何乐而不为呢?

每个人时间有限,每个项目也有deadline,不可能从每一个螺丝钉怎么拧开始学起,不然怎么会有那么多五花八门的框架,会有封装好的组件和接口,正因为有人已经做了前期工作,所以时间才能省下来做更有意义的事情,这就是站在巨人肩上的道理所在吧。

但是我们得明白拿来的东西的原理,以及出了问题该怎么解决的能力。然后才能做出更厉害的东西。

首选当然是官网的例子咯,目测搜了一圈,终于找到一个3D Donut。就是你了,我的巨人。

把该地址的donut3d.js拷贝下来作为画3D饼图的基础js,待会会在此基础上修改,以满足我的要求(长的像齿轮的要求)。

那我们就一睹她的芳容吧。

如果这张图符合你的要求,那就打住,不用往下看了,直接看官网例子即可。

注意d3版本的问题,如果你用d3.v3.js,恭喜你,啥也不用改,直接拿来用;如果你用d3.v5.js,那稍微改下方法,比如d3.v5.js没有d3.layout,所以d3.layout.pie改成d3.pie。我就是那个不幸的人,用的d3.v5.js。没关系,改起来很快,运行下,看哪里有错,就改哪里,O(∩_∩)O哈哈~so easy!

还是先上个我已经改好之后的3D饼图(齿轮图)吧,方便说明。

其实显示的时候是个动态的,一节一节显示出齿轮的。

背景是黑乎乎的,据说现在流行黑乎乎的背景,显得有科技感,技术也要赶时髦啊,我这么fashion的人,做出来的东西也要fashion啊O(∩_∩)O~

从以上分析可以看出,难啃的骨头在第4点。这个图断断续续花了3天时间才搞定,为啥是断断续续呢,因为还有其他工作要做嘛,你懂得。

那就按顺序一条一条实现,总有一天我们的愿望都能实现!

首先新建svg及设置宽高。

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

既然要分成32个小立体快,那数据也要切分成32个。

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

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

请各位仔细看。

是不是有种被掏空的感觉?如果你觉得这样挺好看,那也行,打住吧,后面就不用再看了;如果你想补齐其他面,请耐心往下看。

经过观察和比较,增加左侧面和右侧面就能填满空虚的心啦啦啦~

这次要在donut3d.js这个巨人身上添砖加瓦咯。

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

终于填满需要的每一面,看上去像个立体齿轮图了。

这个图是很久之前做的,当时花了很长时间调试,每一个面有4条边,定位2个点,再加上高度和内半径,就可以计算出4个点,然后就可以画出4条边,最后填充颜色,一个面就完成了。

最近整理文档时觉得有必要写出来,方便以后查阅和探讨,也告诉自己积累是一个长期过程,不急不躁,慢慢来,一步一步完成既定目标,总有一天你会走遍技术的每个角落。

现在我整理成vue组件,传一个百分比的参数,就可以显示3D齿轮图了,我的3D齿轮图也成巨人啦。