JavaScript 可视化库D3介绍

1,970 阅读6分钟

JavaScript 可视化库D3介绍

渲染器的选择

  • SVG
  • Canvas
  • HTML element

一般来说,Canvas 更适合绘制图形元素数量较多(这一般是由数据量大导致)的图表(如热力图、地理坐标系或平行坐标系上的大规模线图或散点图等),也利于实现某些视觉 特效。但是,在不少场景中,SVG 具有重要的优势:它的内存占用更低(这对移动端尤其重要)、并且用户使用浏览器内置的缩放功能时不会模糊。 选择哪种渲染器,我们可以根据软硬件环境、数据量、功能需求综合考虑。 在软硬件环境较好,数据量不大的场景下,两种渲染器都可以适用,并不需要太多纠结。 在环境较差,出现性能问题需要优化的场景下,可以通过试验来确定使用哪种渲染器。比如有这些经验: 在须要创建很多 ECharts 实例且浏览器易崩溃的情况下(可能是因为 Canvas 数量多导致内存占用超出手机承受能力),可以使用 SVG 渲染器来进行改善。大略得说,如果图表运行在低端安卓机,或者我们在使用一些特定图表如 水球图 等,SVG 渲染器可能效果更好。 数据量较大(经验判断 > 1k)、较多交互时,建议选择 Canvas 渲染器。 我们强烈欢迎开发者们反馈给我们使用的体验和场景,帮助我们更好的做优化。

来自echarts 最佳实践 canvas vs svg

render.png

引入

import {scaleLinear} from "d3-scale";
import * as d3 from "d3";
const d3 = await import("d3");

d3当中的核心概念

最简单的一个例子

d3.js当中主要使用 SVG 进行开发,相比直接操作恶心的HTML DOM,D3提供的一套声明式语法更加舒服。

var paragraphs = document.getElementsByTagName("p");
for (var i = 0; i < paragraphs.length; i++) {
  var paragraph = paragraphs.item(i);
  paragraph.style.setProperty("color", "blue", null);
}

显然能够降低心智负担

d3.selectAll("p").style("color", "blue");

练习

d3.selectAll("p")
  .style("color", function(d, i) {
  return i % 2 ? "blue" : "red";
});

selection

概念

d3当中大部分的操作都是针对selection进行的

  • 事件侦听
  • style属性设置
  • 数据绑定
  • ...

通过以下函数可以生成一个selection

d3.create() //创造一个元素
d3.select() //选择匹配的第一个元素
d3.selectAll() //选择一组元素

Selection对象的结构如下

{
    export function Selection(groups, parents) {
        this._groups = groups;
        this._parents = parents;
    }
​
    function selection() {
        return new Selection([[document.documentElement]], root);
    }
​
    function selection_selection() {
        return this;
    }
​
    Selection.prototype = selection.prototype = {
        constructor: Selection,
        select: selection_select,
        selectAll: selection_selectAll,
        selectChild: selection_selectChild,
        selectChildren: selection_selectChildren,
        filter: selection_filter,
        data: selection_data,
        enter: selection_enter,
        exit: selection_exit,
        ...
    };
}

实际生成 Selection 对象

生成的selection对象会有两个隐藏属性,来表示该selection的结构,selection具体是如何工作的,可以查看 Mike Bostock 的 How Selection work

d3通过创建或者选择DOM元素生成一个Selection

这些函数本质上也是调用的W3C DOM API中的Selector API

function empty() {
  return [];
}

export default function(selector) {
  return selector == null ? empty : function() {
    // 调用元素的selector
    return this.querySelectorAll(selector);
  };
}

selection上暴露了大量的方法以供用户操作,方便对元素添加、更新、删除对应的节点


事件注册

selection_work.png

早期版本的d3 是通过全局的d3.event获取当前事件信息,在后续版本已经移除,直接在事件监听器中传递当前的事件信息,d3的版本可以通过全局暴露的d3.version获取

 d3.selectAll("div")
      .on("mouseover", function(){
          d3.select(this)
            .style("background-color", "orange");
          // Get current event info
          console.log(d3.event);
          // Get x & y co-ordinates
          console.log(d3.mouse(this));
      })
      .on("mouseout", function(){
          d3.select(this)
            .style("background-color", "steelblue")
      });

d3通过selection.on()方法注册事件监听器, 任何浏览器支持的标准事件类型都支持

d3.create("ul")
  .call(ul => ul.selectAll("li")
    .data(names)
    .join("li")
      .text(name => `My name is ${name}! `)
    .append("a")
      .attr("href", "#")
      .on("click", click)
      .text("Pick me."))
  .node()

数据绑定

D3直接绑定数据到selection上,数据可以是任意数组类型,给定一个数组和selection就可以将每个数组元素追加到selection当中每个元素。

比如,一个number的数组

[1,2,3,4,5];

或者一个二维数组

const matrix = [
  [11975,  5871, 8916, 2868],
  [ 1951, 10048, 2060, 6171],
  [ 8010, 16145, 8090, 8045],
  [ 1013,   990,  940, 6907]
];

或者一个对象数组

const letters = [
  {name: "A", frequency: .08167},
  {name: "B", frequency: .01492},
  {name: "C", frequency: .02780},
  {name: "D", frequency: .04253},
  {name: "E", frequency: .12702}
];

数据绑定会在对应的dom元素上挂载一个对应的__data__属性

data当中的数据和selection当中的元素对应关系如下

data.png

如果没有为数据绑定提供一个 key function 那么会默认的根据元素在DOM当中的顺序绑定数据

d3.selectAll("div")
  .data(data, function(d) { return d ? d.name : this.id; })
    .text(d => d.number);

过渡

transition 是一个类 selection 的接口,用来对 DOM 进行动画修改。这种修改不是立即修改,而是在规定的事件内平滑过渡到目标状态。

应用过渡,首先要选中元素,然后调用 selection.transition,并且设置期望的改变,例如:

d3.select("body") .transition() .style("background-color", "red"); 过渡支持大多数选择集的方法(比如 transition.attr 和 transition.style 对应 selection.attr 和 selection.style),但是并不是所有的方法都支持; 比如必须在对元素过渡之前 append 元素或者 bind data。transition.remove 操作可以在动画结束时方便的移除元素。

为了计算过渡过程中的状态,过渡集成了大量的 built-in interpolators。Colors, numbers, 以及 transforms 会被自动检测。内嵌数字的 Strings 也会被检测到,可以方便的对许多样式(比如 padding 或 font size) 以及 path 进行过渡。可以使用 transition.attrTween, transition.styleTween 或 transition.tween 指定一个自定义插值器。

模块

d3是各个模块配合一起工作的,可以从collection of modules单独引入,也可以一起使用。 如果是绘制柱状图、折线图、这类图表,可以选择axes来提供定义坐标的样式,选择Scales来定义比例尺。如果还有颜色和交互上的需求可以选择 Zooming Drag 等模块 来缩放和拖拽...

还有一些有意思的模块

  • Forces,来提供力学模拟
  • Geographies 提供地理以及几何的绘制

Forces

D3的力学模拟是通过韦尔莱积分法(Verlet Integration)实现,通过对数据当中的节点添加力学函数来实现特定的力学模拟,每次触发tick事件时,会更新节点的各个属性

 const simulation = d3.forceSimulation(nodes)
      .force("link", forceLink)
      .force("charge", forceNode)
      .force("center",  d3.forceCenter())
      .on("tick", ticked);

在线的可视化平台Observable

d3的主要维护人员,同时也是Overvable的创始人

mike-bostock.jpg

D3提供了一个在线的可视化平台 Observable 为一些简单的例子提供了在线可运行的编辑器,并且封装了一些交互式组件提供了极大的便利

demo地址

observable-tower.png

在Observable中的notebooks中编写的代码是分块执行的,各个代码块组成的程序结构是一个有向无环图(DAG) 更改其中的代码块之后会触发相关联的代码块重新执行

每个代码块要求必须返回一个值,如果返回的时HTML元素,那么会在notebooks当中进行渲染, 一个典型的代码块是

{
  // 返回对应HTML元素
  return svg.node()
}

另外比较比较好用的功能是,Observable提供文件存储功能,可能利用它提供的FileAttachment(file_name)来访问对应的文件,以便对程序当中的数据进行填充

file_attachments.png

更方便的是可以提供类似 Jupter Notebooks 的功能,在编辑中可以书写markdown文档,而且提供部分TeX的数学公式语法支持,如果需要自定义样式的,也可以在代码块当中书写HTML

observable-fn.png