D3.js

数据可视化库王者,SVG 中的 jQuery

需要了解的几个概念

数据可视化

于Web的数据可视化指的是在网页中显示数据统计报表,从而可以更直观地了解数据的走向和趋势,如图表、图谱、地图、关系图、立体图。

数据可视化的展现方式从以往的 Flash 技术、IE 的 vml 语言发展至如今规范统一的 HTML5 技术点——CanvasSVG

SVG

可缩放矢量图形,基于 XML。

axis.png◎ SVG坐标轴

  1. SVG 图形练习:SVG 图形实例
  2. 要使用 SVG 就要考虑到浏览器的兼容问题,Can I Use 是一个查看浏览器兼容状态的强力工具

D3.js

简介

Data Driven Documents,基于数据驱动文档的 JavaScript 库,用于网页作图、生成互动图形,是一个优秀的可视化库。D3.js 不需要你使用哪个特定的框架,也就是说只要能写 JavaScript 就能使用 D3.js,当然是要在浏览器兼容的前提下了。

优势

  • 堪称 SVG 中的 jQuery,操作 SVG 极为方便,当然 Canvas 也支持

    截止目前来说,D3 是操作 SVG 最为方便的一个库,堪比 jQuery 和 JavaScript 的关系,当然这不是说 D3 只能操作 SVG,在 v3 之后的 v4 版本开始 D3 就已经支持 Canvas 了,但是支持不代表擅长,主要的处理目标还是 SVG。

  • 相比于 Echarts 这种框架式工具来说,D3.js 的自由度更高,但相对的学习成本也会变高

    chart 类的工具(Highchart、Echarts)固然简单易上手,但是这类都是“框架式工具”,和“库”区别还是比较大的。举个例子,你需要装修自家房子,一种方式是提供给你装修会用到所有工具并教会你所有装修方法,装修风格就任君发挥了,还有一种方式就是给你出几个样本房,自己从中选择,这就是“库”和“框架式工具”的区别了,所以,玩转 D3 后限制图形样式的就只有你的想象力了。

  • 操作DOM极其强大

    毫不夸张的说,如果你学习过 jQuery,D3 你已经会了⅓。甚至网上有人说 D3 可以替代 jQuery,虽然不现实,但这个说法确实得益于 D3 极强的操作 DOM能力。

入门实例——直方图

1. 新建一个 HTML5 文件

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>D3.js</title>
</head>
<body>
</body>
</html>

2. 引入 D3.js

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

3. 创建 div 用来展示 SVG

<div id='svg'></div>

4. 使用 D3.js 的语法绘制 SVG

const dataset = [223, 34, 55, 66, 99, 276, 123]; // 定义数据
const height = 400; // 定义SVG的高
const width = 400; // 定义SVG的宽
const svg = d3.select("#svg").append("svg")
    .attr("height", height)
    .attr("width", width);

const padding = {
    top: 20,
    left: 20,
    right: 20,
    bottom: 20
};
const rectStep = 35; // 定义间隔
const rectWidth = 30; // 定义矩形宽度
svg.selectAll("rect").data(dataset).enter().append("rect") // 操作矩形
    .attr("fill", "yellow") // 填充颜色
    .attr("x", (d, i) => padding.left + i * rectStep) // 设置矩形x轴坐标
    .attr("y", (d, i) => height - padding.bottom - d) // y轴坐标
    .attr("width", rectWidth) // 矩形宽度
    .attr("height", d => d); // 高度

const text = svg.selectAll("text").data(dataset).enter().append("text") // 添加文本
    .attr("fill", "pink") // 文本颜色
    .attr("font-size", "16px")
    .attr("text-anchor", "middle") // 居中显示
    .attr("x", (d, i) => padding.left + i * rectStep)
    .attr("y", (d, i) => height - padding.bottom - d)
    .text(d => d) // 文本内容
    .attr("dx", rectWidth / 2) // x轴偏移量
    .attr('dy', "-10px") // y轴偏移量

interval&width.png◎ 间距 rectStep 和宽度 rectWidth 说明

◎ 直方图

⚠️注意:

  1. 矩形的 x、y 坐标以矩形的左上角为中心点
  2. SVG 填充背景颜色使用的是fill属性
  3. 矩形之间的间隔为 x 轴的差值

涉及的API:

API含义示例
.select选择元素d3.select(“#id”)
.selectAll选择多个元素d3.selectAll(“#id”)
.append添加元素.append(“svg”)
.attr添加属性.attr(“width”,“400”)
.data绑定数据.data([45,64])
.enter当DOM元素数量少于data的数量时,自动创建元素.enter()
.text设置文本内容.text(d => d)

比例尺和坐标轴

比例尺

先来回顾一下上次直方图中是怎样设置“柱子”的高度的:

svg.selectAll("rect").data(dataset).enter().append("rect") // 操作矩形
    .attr("fill", "yellow") // 填充颜色
    .attr("x", (d, i) => padding.left + i * rectStep) // 设置矩形x轴坐标
    .attr("y", (d, i) => height - padding.bottom - d) // y轴坐标
    .attr("width", rectWidth) // 矩形宽度
    .attr("height", d => d); // 高度

对的,这里是直接使用数值的大小来表示“柱子”的高度,你可能会表示:显示的高度肯定是和数值的大小有关系啊!对!问题就在这个关系上,如果一定是这样一对一的关系,那数值是这样呢:[ 2.5 , 2.1 , 1.7 , 1.3 , 0.9 ],这样呢:[ 2500, 2100, 1700, 1300, 900 ]

这样的数据可视化是毫无可读性、也是绝不可取的,所以需要一种计算关系能够将某一区域的值映射到另一区域,其大小关系不变,这就是 D3 中的一个重要概念——比例尺。

scaleLinear()线性比例尺

线性比例尺中,定义域和值域都是连续的一一对应关系。

例子

var dataset = [1.2, 2.3, 0.9, 1.5, 3.3];
  • 需求

    • 将最小的值映射为0,将最大的值映射为300。
  • 定义比例尺:

    const linear = d3
        .scaleLinear()
        .domain([d3.min(dataset), d3.max(dataset)])
        .range([0, 300])
    
  • 结果

    linear(0.9) // 输出:0
    linear(2.3) // 输出:175
    linear(3.3) // 输出:300
    
  • 解释

    • .domain()为定义域,.range()为值域
    • d3.min()d3.max()返回数组中的最小值和最大值

scaleLinear.png◎ 定义域与值域的映射关系

scaleOrdinal()序数比例尺

输入域和输出域都是离散的数据,不连续,其数据是序数关系。

例子

const dataset = [45, 34, 145, 11, 78];
const color = ['pink', 'blue', 'yellow', 'black', 'green'];
  • 需求
    • 45对应 pink,34对应 blue,以此类推。
  • 定义比例尺

    const ordinal = d3
        .scaleOrdinal()
        .domain(dataset)
        .range(color)
    
  • 结果

    ordinal(45) // 输出:color
    ordinal(145) // 输出:yellow
    ordinal(78) // 输出:green
    

scaleOrdinal.png◎ 定义域与值域的映射关系

v3 以下版本和 v3 以上版本比例尺的写法不同,详情请参阅官方文档。

坐标轴

基本每种图表都需要坐标轴,但是 SVG 中并没有现成的元素,而是需要由其它图形来组成——刻度直线

// 引入D3的v5版本:`<script src="https://d3js.org/d3.v5.min.js"></script>`

const dataset = [223, 34, 55, 66, 99, 276, 123]; // 定义数据
const height = 400; // 定义SVG的高
const width = 400; // 定义SVG的宽
// const 
const svg = d3.select("#svg").append("svg")
    .attr("height", height)
    .attr("width", width);

const padding = { // 定义间距
    top: 50,
    left: 50,
    right: 50,
    bottom: 50
};
const rectStep = 35; // 定义间隔
const rectWidth = 30; // 定义矩形宽度
const xAxisWidth = width - padding.left - padding.right; // x轴长度
const yAxisWidth = height - padding.top - padding.bottom; // y轴长度
const xScale = d3 // x轴比例尺
    .scaleBand() // 序数比例尺
    .domain(dataset.map((d, i) => i)) // 定义域
    .range([0, xAxisWidth]) // 值域
    .padding(0.1); // 坐标轴间距

const yScale = d3 // y轴比例尺
    .scaleLinear() // 线性比例尺
    .domain([0, d3.max(dataset)]) // 定义域
    .rangeRound([yAxisWidth, 0]); // 值域

const genRect = obj => { // 设置矩形的函数
    obj.attr("fill", "yellow") // 填充颜色
        .attr("x", (d, i) => padding.left + xScale(i)) // 设置矩形x轴坐标                
        .attr("y", (d, i) => height - padding.bottom - (yScale(0) - yScale(d))) // y轴坐标
        .attr("width", xScale.bandwidth()) // 矩形宽度
        .attr("height", d => yScale(0) - yScale(d)); // 高度
}

const genText = obj => { // 设置文本的函数
    obj.attr("fill", "pink") // 文本颜色
        .attr("class", "number")
        .attr("font-size", "16px")
        .attr("text-anchor", "middle") // 居中显示
        .attr("x", (d, i) => padding.left + xScale(i)) // 设置矩形x轴坐标                
        .attr("y", (d, i) => height - padding.bottom - (yScale(0) - yScale(d))) // y轴坐标
        .text(d => d) // 文本内容
        .attr("dx", xScale.bandwidth() / 2) // x轴偏移量
        .attr('dy', "-10px") // y轴偏移量
}

const init = dataset => { // 初始化数据函数
    genRect(svg.selectAll("rect").data(dataset).enter().append("rect"));
    genText(svg.selectAll("text").data(dataset).enter().append("text"));
}
init(dataset);

const xAxis = d3.axisBottom(xScale); // x轴
const gx = svg // 存放x轴的容器
    .append("g")
    .attr("transform", `translate(${padding.left},${height-padding.bottom})`); // 默认显示在上方显示,要使他偏移到下面
gx.call(xAxis); // 将x轴放进容器

const yAxis = d3.axisLeft(yScale); // y轴
const gy = svg
    .append("g")
    .attr("transform", `translate(${padding.left},${height-yAxisWidth-padding.bottom})`); // 默认显示在上方显示,要使他偏移到下面
gy.call(yAxis);

用户交互与力导向图

交互式操作

用户交互指的是用户输入了某种指令,程序接收到后做出了某种响应,而 D3.js 中的用户交互当然指的是用户与可视化图表的交互操作了。

例如鼠标移动至图形上时候图形的变色、变形、文字提示,或是点击变大缩小等等,而最经典的运用就是力导向图了。

厉害的交互式图表案例

添加交互

D3 中使用事件监听函数.on(type, function)为元素添加交互事件。

  • 参数 type 为事件类型,下表为常见的事件,参数 function 为事件处理函数

    事件名含义
    鼠标操作
    click鼠标单击
    mouseover鼠标进入
    mouseout鼠标移出
    mousemove鼠标移动(需节流处理)
    mousedown鼠标按下
    mouseup鼠标松开
    dblclick鼠标双击
    键盘操作按住不放会持续触发
    keydown键盘按下
    keyup键盘释放
    keypress键盘按下字符键
    触屏操作
    touchstart触摸屏幕
    touchmove触摸点移动
    touchend离开屏幕
  • d3.select(this)可以直接选择触发事件的元素,但前提是事件处理函数不使用箭头函数

  • 每个select的元素都可以添加.on()事件监听函数

布局

布局的作用是将不适合用于绘图的数据转换为适合用于绘图的数据,简而言之——数据转换

  • D3(v4 以上版本)提供了以下12种布局:

    布局( v3v4以上版本写法不同)API
    饼状图(Pie)d3.layout.pie
    力导向图(Force)d3.forceSimulation
    弦图(Chord)d3.layout.chord
    树状图(Tree)d3.tree
    集群图(Cluster)d3.cluster
    捆图(Bundle)d3.layout.bundle
    打包图(Pack)d3.pack
    直方图(Histogram)d3.layout.histogram
    分区图(Partition)d3.partition
    堆栈图(Stack)d3.layout.stack
    矩阵树图(Treemap)d3.treemap
    层级图(Hierarchy)d3.hierarchy

力导向图

建立在力学模型基础上的一种特殊的图表,常用来呈现复杂的关系网络。由节点和连线组成,节点和连线都被施加了力的作用,根据力来计算节点和连线的运动轨迹。

draw-step.png◎ 不同工具绘制图表步骤

实现

整体思路

  1. 定义数据(节点数据和连线数据)

  2. 定义SVG画布

  3. 设置力导向图布局

  4. 绘制节点(circle)、连线(line)、描述文字(text)和关系文字(text

  5. 监听力导向图的tick事件,每运动一帧重新设置节点坐标、连线起始结尾坐标、描述文字坐标和关系文字坐标

核心代码

const nodes = [{ name: "Zander" }, { name: "Annie" }, { name: "Anna" }, 
{ name: "Doris" }, { name: "Linda" }, { name: "Censek" }, { name: "Paul" }];

const edges = [{
        source: 0, // 数组下标
        target: 4,
        relation: "Cherry组队友"
    },
    {
        source: 0,
        target: 6,
        relation: "上下属"
    },
    {
        source: 1,
        target: 3,
        relation: "Alice组队友"
    },
    {
        source: 1,
        target: 6,
        relation: "上下属"
    },
    {
        source: 2,
        target: 6,
        relation: "上下属"
    },
    {
        source: 2,
        target: 5,
        relation: "两口子"
    },
    {
        source: 3,
        target: 6,
        relation: "上下属"
    },
    {
        source: 4,
        target: 6,
        relation: "上下属"
    },
    {
        source: 5,
        target: 6,
        relation: "上下属"
    },
    {
        source: 2,
        target: 4,
        relation: "相爱相杀"
    },
    {
        source: 3,
        target: 5,
        relation: "室友"
    }
];
const height = 800;
const width = 800;
const padding = {
    top: 50,
    left: 50,
    bottom: 50,
    right: 50
};
// svg画布
let svg = d3
    .select("#svg")
    .append("svg")
    .attr("height", height)
    .attr("width", width);
// 设置力导向图布局和参数
let force = d3
    .forceSimulation() // 指定为力导向图布局
    .nodes(nodes) // 设置节点
    .force("link", d3.forceLink(edges).distance(200)) // 设置连线和连线的长度
    .force("charge", d3.forceManyBody) // 添加多体力(吸力、斥力等组合起来的高阶函数)
    .force(
        "center", // 设置力中心点
        d3
        .forceCenter() // 创建一个力中心
        .x((width - padding.left - padding.right) / 2) // x坐标
        .y((height - padding.top - padding.bottom) / 2) // y坐标
    );
console.log(nodes); // 数据改变
// 绘制节点
var circles = svg
    .selectAll("circle")
    .data(nodes)
    .enter()
    .append("circle")
    .attr("r", 10)
    .style("fill", "yellow");
// 绘制连线
var lines = svg
    .selectAll("line")
    .data(edges)
    .enter()
    .append("line")
    .style("stroke", "green")
    .style("stroke-width", 1);
// 添加描述
var text = svg
    .selectAll("text")
    .data(nodes)
    .enter()
    .append("text")
    .style("font-size", "12px")
    .style("fill", "#000")
    .attr("dx", 0)
    .attr("dy", 0)
    .text(d => d.name);
// 添加关系
var relations = svg
    .selectAll(".relation")
    .data(edges)
    .enter()
    .append("text")
    .style("fill", "red")
    .style("font-size", "11px")
    .attr("class", "relation")
    .attr("dx", 0)
    .attr("dy", 0)
    .text(d => d.relation);
force.on("tick", () => { // 监听力导向图每运动一帧
    lines
        .attr("x1", d => d.source.x)
        .attr("y1", d => d.source.y)
        .attr("x2", d => d.target.x)
        .attr("y2", d => d.target.y);
    circles
        .attr("cx", d => d.x)
        .attr("cy", d => d.y)
        .call( // 将拖拽行为加入到节点上
            d3
            .drag() // 设置拖拽行为
            .on("start", dragStarted) // 拖拽开始
            .on("drag", dragging) // 拖拽中
            .on("end", dragEnded) //拖拽结束
        );
    text.attr("x", d => d.x).attr("y", d => d.y);
    relations
        .attr("x", d => (d.source.x + d.target.x) / 2) // x坐标为连线中间点x坐标
        .attr("y", d => (d.source.y + d.target.y) / 2); // y坐标为连线中间点y坐标
});
function dragStarted(d) { // 拖拽开始事件处理函数
    if (!d3.event.active) force.alphaTarget(0.3).restart(); // 设置力的衰减系数α(范围[0, 1],值越大移动速度越高,并重新布局该区域
    d.fx = d.x; // d.fx为静止时的坐标,d.x为初始坐标
}
function dragging(d) { // 拖拽中事件处理函数
    d.fx = d3.event.x; // d3.event.x为拖动时的坐标
    d.fy = d3.event.y;
}
function dragEnded(d) { // 拖拽结束事件处理函数
    if (!d3.event.active) force.alphaTarget(0);
    d.fx = null;
    d.fy = null;
}
}

force.gif◎ 效果

绘制热点图

没事儿画啥地图?

  • 比如现有一需求:
    • 在吃鸡地图中标示出哪里跳伞的人多及哪里跳伞的人少,然后就可以根据这个图大概分析出跳哪里落地成盒的几率更小🌚。

pubg-hot-map.jpeg◎ 某手游热力图

  • 正经点儿,实际业务需求:

    • 在中国地图上显示出百星23个自有制作中心、16个驻场制作中心、39个外包服务站点所在的地理位置(实力打广告🤨),并通过点的大小、亮度等体现出不同制作中心的业务量高低。
  • 没错,热点图(又称地图散点图)是这类需求的标准解决方案,其可通过在地图上不同区域使用不同的标志来呈现不同区域的关注程度,使数据清晰明了地呈现在人眼前。

准备工作

1. 中国地理信息文件——GeoJSON

GeoJSON是用于描述地理空间信息的数据格式,包括地图上的所有要素,如经纬度、点、线、面、特征等,格式为 JSON 格式。

网上搜索 china.json 一大堆😏,但是要注意格式要符合 GeoJSON 标准

2. 球形➡️平面投影——d3-geo

经纬度不能直接用于绘图,需要转换为平面坐标,D3 中提供了丰富的投影函数来处理球面坐标和经纬度运算。

安装:npm install d3-geo d3-array --save

3. 配色方案

安装:npm install d3-scale-chromatic --save

绘制地图

1. 按需导入类库

<!-- HTML结构 -->
<div>
  <div id="svg"></div>
  <div id="hover"></div>
</div>
// .vue文件中
import * as geo from "d3-geo";
import * as d3Color from "d3-scale-chromatic";

2. 定义 SVG 画布和投影函数

// 投影函数
const projection = geo
    .geoMercator() // 球形墨卡托投影
    .scale(550) // 投影的比例因子,可按比例放大投影
    .center([105, 38]) // 设置中心点的经纬度为中国地图中心点
    .translate([width / 2, height / 2]); // 将投影偏移至设SVG中心

3. 创建路径生成器和颜色比例尺

const path = geo.geoPath(projection);
const colors = d3.scaleOrdinal(d3Color.schemeBrBG[11]); 

4. 获取 GeoJSON 数据,生成地图

此处为重中之重,要使用 D3.js 提供的d3.json()方法获取地理信息文件,官方文档表明此方法根据指定的 url 创建一个 JSON 文件请求,这就要求不能使用本地的 JSON 文件而要搭建服务器返回数据。本案例使用 Express + MongoDB 搭建了简单的服务器返回地理信息数据。

async function getJson() { // 使用d3.json()方法拿到GeoJSON数据
    const result = await d3.json("/api/allData");
    // const result = await d3.json("../../static/china.json"); // d3.json()不可访问本地文件
    const data = result.result; // 拿到数据,Object类型
    console.log(data);
    return data;
}
getJson().then(data => {
    svg
        .selectAll("path")
        .data(data.features) // 绑定地理特征数据
        .enter()
        // .append("path")
        .insert("path", "g")
        .attr("d", path)
        .attr("fill", function (d, i) { // 加入颜色比例尺
            return colors(i);
        })
        .attr("stroke", "rgba(255, 255, 255, 1")
        .attr("stroke-width", 1);
});

使用append("path")的话 DOM 结构中 path 元素会处于 g 元素之前,导致 path 覆盖 g 元素,原因是 D3 中元素的层级是由元素创建的先后顺序来决定的。

定位城市坐标

1. 定义城市坐标数据

const places = [
  { name: "北京制作中心", log: "116.54", lat: "39.82" }, // log为经度, lat为纬度
  { name: "西安制作中心", log: "108.84", lat: "34.21" },
  { name: "沈阳制作中心", log: "123.39", lat: "41.86" }
];

2. 标点并绘制

const location = svg
    .selectAll("g")
    .data(places)
    .enter()
    .append("g")
    .attr("transform", d => {
        const coord = projection([d.log, d.lat]); // 将经纬度转化为页面坐标
        console.log("coord", coord);
        return `translate(${coord[0]},${coord[1]})`;
    });
location
    .append("circle")
    .attr("r", 4)
    .attr("fill", "yellow");

3. 加入动画

const hover = d3.select("#hover");
location
    .on("mouseover", function (d) {
        hover
            .html(d.name)
            .style("position", "fixed")
            .style("color", "blue")
            .style("left", d3.event.pageX - 45 + "px")
            .style("top", d3.event.pageY - 40 + "px")
            .style("opacity", 1);

        d3.select(this)
            .select("circle")
            .transition()
            .duration(500)
            .attr("r", 8);
    })
    .on("mouseout", function () {
        hover.style("opacity", 0);
        d3.select(this)
            .select("circle")
            .transition()
            .duration(1000)
            .attr("r", 4);
    });

belstar-hot-map.gif◎ 百星制作中心热点图


🎱案例 GitHub 地址:https://github.com/Xuezenghuigithub/D3.js_china_map

References & Resources

  1. d3/d3/wiki/CN-Home | GitHub
  2. D3中常用的比例尺 | SegmentFault
updatedupdated2020-03-062020-03-06
format style
加载评论