cover_image

见微知著 - D3JS这个极客很爱的可视化框架

大转转FE 大转转FE
2021年05月26日 01:00

图片

一、D3是什么?

  1. 全称:Data-Driven Documents - 数据驱动的文档。 github仓库
  2. D3是一个被数据驱动的可视化工具库,可以帮助你使用HTML、CSS、SVG和Canvas来展示数据。
  3. D3可以将数据绑定到DOM上,然后根据所绑定的数据来计算对应DOM的属性值。(你可以根据一组数据生成一个表格,或生成一个可以过渡和交互的SVG图形等。)
图片

二、D3的优势

  1. 数据能够和Dom绑定在一起,使得数据和图形成为一个整体。在更改图形时,只需对数据操作,图形便会相应的更新。
  2. D3自由度度很大,基本可以自己绘制任何想要的图形,这类情况的需求可以使用D3进行二次开发,定制适合的图表,但是开发成本会稍高。因此,开发中要根据实际情况来判断。无论采用哪种方式开发都要做好二次封装,把实现的图表成可复用的组件。
  3. 数据转换和绘制是独立的:例如将数据转换为图表,需要不少的数学算法。在多种方案之中,D3独立地提供了数据转换和图表绘制的工具函数,即将开发者的数据转换为图表绘制工具函数所需的数据格式,然后按照开发者的需求进行绘制。
  4. 代码简洁:采用了链式调用的方式简化图形的绘制。
  5. 大量布局工具:饼状图、树形图、打包图、矩阵图等。D3将大量复杂的算法封装成了一个一个的布局,适用于各种的图表制作。
  6. 主要基于Svg,缩放不会损失精度。
图片

三、D3与ECharts的对比

  1. D3可定制自由度高,而ECharts不可定制,自由度低。
  2. D3英文文档完善,使用事例只能参考,而ECharts中文文档完善、使用例子功能完善。
  3. D3需要开发,效率低,ECharts可以快速配置生成图表。
  4. D3较难,必须熟悉Svg、Canvas、D3Api, ECharts 只需配置。
图片

四、D3的使用场景

  1. ECharts等配置式图表库无支持图表时使用。
  2. 数据量不是特别大或者事件交互比较精细的场景采用D3,可以先在官方示例和demo搜索有没有类似的图表实现。
  3. 对于频繁的DOM操作十分消耗性能。对于用户体验的影响便是可能出现闪烁、卡顿等现象。可以参考前端界对于页面DOM卡顿的解决方案:Virtual DOM 技术。通过支持 Virtual Dom 技术的框架如Vue与 D3.js结合。使用D3来计算,Vue等Virtual DOM框架管理SVG节点和属性。
  4. 对于数据量比较大的场景,可以采用D3Canvas来实现,或者ZRender(ECharts使用的矢量图形库)来定制,这个需要比较熟悉Canvas绘图,而且需要注意性能的优化。
图片

五、D3的开发流程

  1. 根据需求确定图表类型。
  2. 把输入的原始数据转化为标准的D3可接受的数据格式。
  3. 根据原始数据定义好x轴函数、y轴函数和定义好作图方式在Svg上,画出x轴y轴、根据原始数据结合x轴和y轴函数做图。
  4. 画出标题等细节东西。
  5. 给已经完成的图形添加动画效果和时间交互。
图片

六、D3的版本更替

  1. D3现存三种版本:V3、V4、V5。
  2. 在V3更新到V4的时候大刀阔斧地规范了一些方法,各个Api的调用形式有不同的变化。
  3. 相比V3到V4的变化,V4到V5只有少量向前不兼容的改动。
/* v3 比例尺 */
d3.scale.linear()
/* v4 比例尺 */
d3.scaleLinear()

七、D3的使用实例 earth-master

  1. 该项目使用D3Geo模块进行地球的渲染。
  2. D3Geo模块支持加载GeoJson、TopoJson等标准的地理信息数据格式。
  3. D3Geo模块根据地理信息渲染出不同的投影(如墨卡托投影,高斯投影等)。
  4. 可方便对地理要素进行着色。

墨卡托投影绘制的地球及风场动画的制作:图片帕特森投影绘制的地球及风场动画:图片

八、D3的常用概念介绍

8.1 基本概念:

8.1.1 选择集

  1. 选择集和数据是D3最重要的概念。选择集是被选择元素的集合。
// 返回匹配选择器的第一个元素
d3.select('.class')
// 返回匹配选择器的所有元素
d3.selectAll('.class')

8.1.2 数据绑定

  1. 将数据和dom绑定是D3的特色,数据绑定就是使被选择元素含有数据。
var data = [{value11}, {value12}] 
// 选择集的每一个元素都绑定相同的数据
selection.datum(data)
// 选择集的每一个元素都绑定values的每一项
selection.data(data)

8.1.3 数据驱动

  1. 当数组长度与元素数据不一样时,有enter部分和exit部分。
  2. 如果没有足够的元素,就使用enter处理方法添加元素。
  3. 如果元素的数据需要更新,则使用update处理方法更新元素的数据。
// 颜色插值
var compute = d3.interpolate(d3.rgb(255255153), d3.rgb(255204102));
// 颜色比例尺
var linear = d3.scaleLinear()
      .domain([010])
      .range([01]);

// 根据数据计算圆心x坐标
function calCX(d, i{
    return i * 30 + 5
}

// 根据数据计算圆心y坐标
function calCY(d{
    return (d - 1) * 30 + 5
}

// 根据数据计算圆形半径
function calCR(d){
    return d * 2.5
}

// 根据数据计算填充颜色
function calFill(d){
    return compute(linear(d))
}

// 根据data绘制图形
function render(data){
    var circles = d3.select('#target').selectAll('circle').data(data)
    // 处理待更新的数据集
    circles.style('fill''rgb(255,102,102)')
        .attr('r', calCR)
        .attr('cx', calCX)
        .attr('cy', calCY)

    // 处理新添加的数据集
    circles.enter()
        .append('circle')
        .style('fill', calFill)
        .attr('r', calCR)
        .attr('cx', calCX)
        .attr('cy', calCY)
                
    // 处理已退出的数据集
    circles.exit()
        .remove()
}
// 添加Svg画布
var svg = d3.select('body')
    .append("svg")
    .attr("id""target")
    .attr("width""300px")
    .attr("height""300px")
    .attr("style""border:1px solid black")
    
// 绘制数据
render([1510])
render([12345678910])

运行结果如下:图片

8.1.4 数据的转换和绘制

// 数据处理
var hierarchy = d3.hierarchy(dataset)
console.log('hierarchy', hierarchy)

打印结果如下:图片

// 将数据转换为绘制所需要的数据格式
var createTree = d3.tree()
    .size([200200])
    .separation(function(a, b)return a.parent == b.parent? 12})
var tree = createTree(hierarchy)
console.log('tree', tree)
// 生成节点所需数据      
var descendants = tree.descendants()
console.log('descendants', descendants)
// 生成连线所需数据
var links = tree.links()
console.log('links', links)

打印结果如下:图片

经过绘制:图片

8.2 绘制

8.2.1 颜色

// 创建各种格式的颜色
var color = d3.color('steelblue')
  1. 如果要计算介于两个颜色之间的颜色,需要用到插值。
  2. D3提供和很多预设的颜色主题,可以与比例尺配合进行方便地颜色绘制。
 // D3提供的颜色主题 配合比例尺进行绘制
 var scheme = d3.schemeCategory10
 var scale = d3.scaleOrdinal(scheme)]

经过绘制:图片

8.2.2 路经生成器

  1. D3中引入了路经生成器的概念,能够自动根据数据生成路经。
  2. 线段生成器:用于生成线段的路经生成器。
  3. 区域生成器:用于生成区域的路经生成器。
  4. 弧生成器:用于生成弧的路经生成器。
  5. 还有许多其他路经生成器官方文档有介绍。

8.3 布局

8.3.1 概念

  1. 布局是D3中很重要的内容,使用布局能够轻松创建很多图表。我们可以简单地将布局理解为“数据转换”。
  2. 布局的意义在于计算出方便绘图的数据。
  3. D3提供的布局有:饼状图(Pie)、力导向图(Force)、弦图(Chord)、树状图(Tree)、集群图(Cluster)、捆图(Bundle)、打包图(Pack)、直方图(Histogram)、分区图(Partition)、堆栈图(Stack)、矩阵数图(Treemap)、层级图(Hierarchy)。

8.3.2 使用布局的基本步骤:

  1. 确定初始数据:转换前的数据要符合一定的格式要求,才能被布局函数转换。
  2. 转换数据:指布局根据初始数据计算绘图所需数据。
  3. 绘制:使用转换后的数据在SVG画板上绘图(很可能会用到生成器)
// 饼状图布局(数据转换)生成arc所需要的数据格式
var createPie = d3.pie()
    .value(function(d)return d.value })
var pie = createPie(dataset)

// 路经生成器
var arc = d3.arc()
    .innerRadius(0)
    .outerRadius(142)

经过绘制:图片

8.4 图表

8.4.1 比例尺

  1. 定义域和值域的对应法则
  2. 定量比例尺:线性比例尺、指数和对数比例尺、两字和分位比例尺、阈值比例尺。
  3. 序数比例尺。
// 线性比例尺
var linear = d3.scaleLinear()
    .domain([10130])
    .range([0960])

linear(20// 80
linear(50// 320

8.4.2 坐标轴

  1. d3.axisLeft() 创建坐标轴(可以创建不同方向的)。
  2. d3.scale([scale]) 设定或获取坐标轴比例尺。
  3. 坐标轴比例尺:定义域为坐标轴刻度值的范围,值域是坐标轴实际的像素长度。
  4. 有了比例尺后,图表元素的位置、长度等属性都由比例尺计算得到。
// 坐标轴比例尺
var xScale = d3.scaleLinear()
    .domain([-5050])
    .range([0300])
var yScale = d3.scaleLinear()
    .domain([-5050])
    .range([3000])
// x、y坐标轴
var xAxis = d3.axisBottom(xScale)
    .ticks(11)
var yAxis = d3.axisLeft(yScale)
    .ticks(11)

经过绘制:图片

8.5 与Canvas的混用

  1. D3是一款基于Svg的可视化工具库,Svg比起Canvas来说,可以更方便地完成复杂的交互方式,以及由于Svg属于矢量图形绘制技术,在放大缩小时不会产生失真。
  2. 不过Canvas也有很多它的优点,所以在一些情况下,我们会联合使用D3和Canvas, 我们用D3将数据转换为便于绘制的格式,或者使用一些D3现成的算法,在Canvas画布上进行绘制。

示例代码如下,省略了部分工具函数代码。

// D3-geo模块投影相关功能(将笛卡尔坐标系的坐标投影至其他坐标系)
const projection = d3.geoOrthographic()
    .rotate([040])
    .translate([250250])
    .fitExtent([[20,20], [500-20500-20]], {type:"Sphere"})
// 利用Canvas的context2D绘制三角形
function drawTriangle([p0, p1, p2]){
    context.moveTo(...p0)
    context.lineTo(...p1)
    context.lineTo(...p2)
    context.closePath()
}
// 获得可绘制的三角形面坐标
const faces = geodesic(10)
// 渲染函数,可增加设置投影代码,进而增加动效。
function render(){
    const triangles = faces.map((d,i)=>(d=d.map(projection),d.index = i, d))
        .filter(d=>d3.polygonArea(d) < 0)

    context.clearRect(0,0,500,500)
    for(const t of triangles){
        context.beginPath()
        drawTriangle(t)
        context.fillStyle = d3.interpolateRainbow(faces[t.index][0][0] / 360);
        context.fill()
    }
    context.canvas
}
render()

经过绘制:图片

8.6 动画的使用

  1. D3和动画结合能方便地作出一些动画场景。

示例代码如下(齿轮转动)。

// 初始参数设置
let angle = 0
let frameAngle = 0
let x = 0.8660254037844387
let y = -0.499999999999999
let toothRadius = 0.008
let holeRadius = 0.02
let speed = 0.05
gears = [
    {fill"rgb(142,211,199)"teeth80radius-0.5origin: [00], annulustrue},
    {fill"rgb(252,180,98)"teeth16radius: +0.1origin: [0,0]},
    {fill"rgb(189,186,217)"teeth32radius-0.2origin: [0-0.3]},
    {fill"rgb(250,129,113)"teeth32radius-0.2origin: [-0.3 * x, -0.3 * y]},
    {fill"rgb(127,178,211)"teeth32radius-0.2origin: [0.3 * x, -0.3 * y]}
]
// 生成齿轮路经
function gear({teeth, radius, annulus, origin}{
    const n = teeth
    let r2 = Math.abs(radius)
    let r0 = r2 - toothRadius
    let r1 = r2 + toothRadius
    let r3 = holeRadius
    
    if(annulus) {
        r3 = r0
        r0 = r1
        r1 = r3
        r3 = r2 + toothRadius * 3
    }

    const da = Math.PI / n
    let a0 = -Math.PI / 2 + (annulus ? Math.PI / n : 0)
    const path = ["M", r0 * Math.cos(a0), ",", r0 * Math.sin(a0)]
    let i = -1

    while(++i < n) {
        path.push(
            "A", r0, ",", r0, " 0 0,1", r0 * Math.cos(a0 += da), ",", r0 * Math.sin(a0),
            "L", r2 * Math.cos(a0), ",", r2 * Math.sin(a0),
            "L", r1 * Math.cos(a0 += da/3), ",", r1 * Math.sin(a0),
            "A", r1, ",", r1, " 0 0,1", r1 * Math.cos(a0 += da / 3), "," , r1 * Math.sin(a0),
            "L", r2 * Math.cos(a0 += da / 3), ",", r2 * Math.sin(a0),
            "L", r0 * Math.cos(a0), "," , r0*Math.sin(a0)                 
        )
    }
    path.push("M0,", -r3, "A", r3, ",", r3, " 0 0,0 0,", r3, "A", r3, ",", r3, " 0 0,0 0,", -r3, "2")
    return path.join("")
}
// 挂载Svg
const svg = d3.select("body").append("svg")
    .attr("viewBox", [-0.53-0.531.061.06])
    .attr("stroke""black")
    .attr("stroke-width"1 / 640)
    .attr("max-width""640px")
    .style("display""block")
    .style("margin""auto")
// 绘制
const frame = svg.append("g")
    .attr("transform"`rotate(${frameAngle % 360})`);
const path = frame.selectAll("path")
    .data(gears)
    .join("path")
    .attr("fill", d=>d.fill)
    .attr("d", gear)
    .attr("transform", d=>`translate(${d.origin}) rotate(${(angle/d.radius) % 360})`)
/
/ 动画
function update(){
    path.attr("transform", d=>`translate(${d.origin}) rotate(${(angle/
d.radius) % 360}
)`
)
    angle += speed
    frameAngle += speed 
    window.requestAnimationFrame(update)
}
update()

经过绘制:图片

九、结束:D3的基本使用今天就介绍到这里,如果大家感兴趣的话,github上有详细完整的文档可供参考。

戳我进入github仓库

十、感谢

  1. 用友APM友云音前端技术团队,博客中提到的ECharts和D3的区别。
  2. 《精通D3.js:交互式数据可视化高级编程》的作者吕之华。
  3. github项目earth-master的所有贡献者。
  4. 百度上搜到一些零碎的点的作者。
图片


继续滑动看下一个
大转转FE
向上滑动看下一个