热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

十大案例,带你使用D3.js实现数据可视化

原文:LearntoCreateD3.jsDataVisualizationsbyExample译者:kmokidd,UI工程师。责编:陈秋歌,CSDN内容运营,

原文:Learn to Create D3.js Data Visualizations by Example
译者:kmokidd,UI 工程师。
责编:陈秋歌,CSDN内容运营,关注前端开发、用户体验技术领域。研发心得、项目实战、前沿技术、外文翻译……,只要是技术干货,十分欢迎投稿至chenqg#csdn.net。人人都是主编,这里就是你的舞台。

基于 D3.js ,编写 HTML、SVG 和 CSS 就能让你的数据变得生动起来,这是一个基于数据操作 DOM 的 Javascript 库。


我看来,每一位 Web 开发者最应该学习的三个 Javascript 库就是 jQuery、Underscore 和 D3。在学习它们的过程中,你将会从新的角度去思考如何写代码:jQuery 让你知道如何用尽量少的代码,尽可能多地操作 DOM;Underscore (或者称之为 lodash)用函数式的工具改变你写程序的方式;而 D3 给了你大量的操作数据的工具,以及图形化编程的思想。如果你还不了解 D3,请花一些时间看看它的例子,体会一下 D3 能做到什么。

这可不是你老爸的图表库。

D3 有极高的灵活性,它是一个比较基础的可视化 js 库,api 和 jQuery 很像,可以把数据和 HTML 结构或者 SVG 文档对应起来。D3 有丰富的数学函数来处理数据转换和物理计算,它擅长于操作 SVG 中的路径 (path) 和几何图形 (circle、ellipse、rect…)。

这篇文章旨在给读者一个 D3 的概览,在接下来的例子里,你会看到输入的数据、数据变换和最后的输出文档。我将不会解释逐个函数做了什么,我会把代码展示给你看,希望从中你能知道大致这些代码是怎么工作的。只有在 Scales 和 Selections 的部分我才会重点解释。

柱状图

图片描述

codepen中查看代码

我说过在 D3 中,你能使用到的图表远比 Playfair 先生发明过的要多,但是学跑前先走好路,我们从最简单的柱状图开始,了解一下 D3 是怎么将数据和文档流结合在一起的:

d3.select('#chart')
  .selectAll("div")
  .data([4, 8, 15, 16, 23, 42])
  .enter()
  .append("div")
  .style("height", (d)=> d + "px")

selectAll 方法返回了一个 D3 selection。D3 selection 是一个数组,当针对一个数据点创建一个 div ,然后通过 enter() 和 append() 调用这个 div 时,selection 中的元素就会被创建出来。

上述代码中的输入数据是一组数组:[4, 8, 15, 16, 23, 42],对应的输出 HTML 结构是:

<div id="chart">
  <div hljs-string">"height: 4px;">div>
  <div hljs-string">"height: 8px;">div>
  <div hljs-string">"height: 15px;">div>
  <div hljs-string">"height: 16px;">div>
  <div hljs-string">"height: 23px;">div>
  <div hljs-string">"height: 42px;">div>
div>

所有不需要通过 JS 控制的视觉层内容都写到 CSS 中:

#chart div { display: inline-block; background: #4285F4; width: 20px; margin-right: 3px; }

GitHub 贡献表

只需要更改柱状图代码中的几行,我们就能得到一个 GitHub 贡献表。

图片描述

codepen中查看代码

和柱状图不同地方在于,图表中根据数据变化的不再是元素的高度,而是元素的 background-color (背景色)。

const colorMap = d3.interpolateRgb(
  d3.rgb('#d6e685'),
  d3.rgb('#1e6823')
)

d3.select('#chart')
  .selectAll("div")
  .data([.2, .4, 0, 0, .13, .92])
  .enter()
  .append("div")
  .style("background-color", (d)=> { return d == 0 ? '#eee' : colorMap(d) })

colorMap 函数接收的输入值要在0到1之间,返回的是一个颜色值,这个值是在以输入值中两两数据为颜色值之间的渐变色值。插值法是图形编程和动画的关键点。在后面我们将会看到更多这方面的例子。

SVG

D3 最大的魅力大概来自于它能应用在 SVG 上,也就是说平面图形比如圆形、多边形、路径和文本,它都可以交互。
图片描述

<svg width="200" height="200">
  <circle fill="#3E5693" cx="50" cy="120" r="20" />
  <text x="100" y="100">Hello SVG!text>
  <path d="M100,10L150,70L50,70Z" fill="#BEDBC3" stroke="#539E91" stroke-width="3">
svg>

上述代码实现的是:

  • 一个圆心在 (50,120),半径是 20 的圆形;
  • 一段位于 (100,100) 的文本;
  • 一个 3px 粗边的三角形,d 指的是方向,从点 (100,100) 画线到点 (150,170) 再到点 (50,70) 结束

可以算是 SVG 中最好用的元素了。

圆形

图片描述

codepen中查看代码

上面的例子中给出的数据结构是很简单的一组数据,D3 的能力远不止于此,它还可以操作更复杂的数据类型。

const data = [{ label: "7am", sales: 20 },{ label: "8am", sales: 12 }, { label: "9am", sales: 8 }, { label: "10am", sales: 27 }]

对每一个数据点,我们都将有一个 g (组)元素在 #chart 中,根据对象的属性,每个组里会有一个 元素和一个 元素。

下面的代码将输入数据和 SVG 文档一一对应起来,你能看出它的原理吗?

<svg height="100" width="250" id="chart">
  <g>
    <circle cy="40" cx="50" r="20"/>
    <text y="90" x="50">7amtext>
  g>
  <g>
    <circle cy="40" cx="100" r="12"/>
    <text y="90" x="100">8amtext>
  g>
  <g>
    <circle cy="40" cx="150" r="8"/>
    <text y="90" x="150">9amtext>
  g>
  <g>
    <circle cy="40" cx="200" r="27"/>
    <text y="90" x="200">10amtext>
  g>
svg>

折线图

图片描述

codepen 中查看代码

用 SVG 实现折线图再简单不过,我们将下面这些数据:

const data = [
  { x: 0, y: 30 },
  { x: 50, y: 20 },
  { x: 100, y: 40 },
  { x: 150, y: 80 },
  { x: 200, y: 95 }
]

转换成以下的 SVG 文档:

<svg id="chart" height="100" width="200">
  <path stroke-width="2" d="M0,70L50,80L100,60L150,20L200,5">
svg>

**注意:**SVG 代码中的 y 值和输入值的 y 值不同,是用 100 减去给定的 y 值,因为在 SVG 中屏幕的左上角是 (0,0),所以在纵坐标最大值是 100 的坐标系中,需要这么处理。

可以这么实现只由一条 path 构成的图形:

const path = "M" + data.map((d)=> { return d.x + ',' + (100 - d.y); }).join('L'); const line = `<path stroke-width="2" d="${ path }"/>`; document.querySelector('#chart').innerHTML = line;

上面的代码看着可麻烦了,D3 其实提供了路径生成函数来简化这个步骤:

const line = d3.svg.line()
  .x((d)=> d.x) .y((d)=> 100 - d.y) .interpolate("linear") d3.select('#chart') .append("path") .attr('stroke-width', 2) .attr('d', line(data))

清爽多了!interpolate 函数可接受不同的参数,画出不一样的图形,除了 ‘linear’,还可以试试看 ‘basis’、’cardinal’……

图片描述

Scales

Scales 函数可以将一个输入集映射到一个输出集中。

codepen中查看代码

上述例子所用的数据都是假数据,不会超过坐标轴所设定的范围。当数据是动态变化的时候,事情可就没有这么简单了,你需要将输入映射到固定范围的输出中,也就是我们的坐标轴。

假设我们有一个 500px X 200px 大小的折线图区域,输入数据是:

const data = [
  { x: 0, y: 30 },
  { x: 25, y: 15 },
  { x: 50, y: 20 }
]

如果 y 轴的范围在 [0,30],x 轴的范围在 [0,50],那数据就能被漂亮地呈现在屏幕上。不过现实是,y 轴范围在0到200,x 轴范围在0到500间。

我们可以用 d3.max 获取到输入数据中的最大的 x 值和 y 值,然后创建出对应的 scales。

scale 和上面用到的颜色差值函数类似,都是将输入值对应到固定的输出范围中。

xScale(0) -> 0
xScale(10) -> 100
xScale(50) -> 500

对于超出输出值范围的输入值,同样适用:

xScale(-10) -> -100
xScale(60) -> 600

在生成折线图的代码中 scales 可以这么使用:

const line = d3.svg.line()
  .x((d)=> xScale(d.x)) .y((d)=> yScale(d.y)) .interpolate("linear")

scales 还能让图形更优雅地显示出来,比如加上一点间距:

const padding = 20;
const xScale = d3.scale.linear()
  .domain([0, xMax])
  .range([padding, width - padding])

const yScale = d3.scale.linear()
  .domain([0, yMax])
  .range([height - padding, padding])

现在可以在有动态数据集的前提下生成该集的折线图,这条折线图保证会在 500px X 200px 的范围内,并且距离该区域的四边都有 20px 的间距。

线性的 scale 最常见,不过还有处理指数的 pow、处理非数值数据(比如分类、命名等)的 ordinal scales,除此之外还有 Quantitative Scales、Ordinal Scales 和 Time Scales。

比如把我的寿命当做输入值,映射到 [0,500] 的区域内:

const life = d3.time.scale()
  .domain([new Date(1986, 1, 18), new Date()])
  .range([0, 500])

// 0 到 500 之间的哪个点才是我的 18 岁生日呢?
life(new Date(2004, 1, 18))

航线数据可视化

到目前为止,我们看到的都是静止的图像。接下来,以墨尔本到悉尼的航线为例,试试动态图像吧。

codepen中查看效果

上面的效果是基于 SVG 的文本 (text)、线条 (line) 和圆形 (circle)。

<svg id="chart" width="600" height="500">
  <text class="time" x="300" y="50" text-anchor="middle">6:00text>
  <text class="origin-text" x="90" y="75" text-anchor="end">MELtext>
  <text class="dest-text" x="510" y="75" text-anchor="start">SYDtext>
  <circle class="origin-dot" r="5" cx="100" cy="75" />
  <circle class="dest-dot" r="5" cx="500" cy="75" />
  <line class="origin-dest-line" x1="110" y1="75" x2="490" y2="75" />

  
  <g class="flight">
    <text class="flight-id" x="160" y="100">JQ 500text>
    <line class="flight-line" x1="100" y1="100" x2="150" y2="100" />
    <circle class="flight-dot" cx="150" cy="100" r="5" />
  g>
svg>

动态变化的部分是时间和不同的航班,数据源大概长这样:

let data = [
  { departs: '06:00 am', arrives: '07:25 am', id: 'Jetstar 500' },
  { departs: '06:00 am', arrives: '07:25 am', id: 'Qantas 400' },
  { departs: '06:00 am', arrives: '07:25 am', id: 'Virgin 803' }
]

我们需要将每条航线的出发和到达时间用 scale 映射到 x 轴上,这些数据将会动态地变化。在代码开始的位置,就将这些数据依次设定为 Date 对象,并 scale 它们,后面方便使用。对日期的处理我用的是 Moment.js。

data.forEach((d)=> {
  d.departureDate = moment(d.departs, "hh-mm a").toDate();
  d.arrivalDate = moment(d.arrives, "hh-mm a").toDate();
  d.xScale = d3.time.scale()
    .domain([departureDate, arrivalDate])
    .range([100, 500])
});

现在可以把数据传入 xScale() 获取到每条航线的 x 坐标。

渲染循环

出发和到达时间都四舍五入到 5 分钟,所以在第一个航班起飞时间和最后一个航班到达时间之间以 5分钟为单位递增。

let now = moment(data[0].departs, "hh:mm a");
const end = moment(data[data.length - 1].arrives, "hh:mm a");

const loop = function() {
  const time = now.toDate();

  // 选出当前的航班
  const currentData = data.filter((d)=> { return d.departureDate <= time && time <= d.arrivalDate }); render(currentData, time); if (now <= end) { // Increment 5m and call loop again in 500ms // 5分钟为增量,500ms 轮询依次 now = now.add(5, 'minutes'); setTimeout(loop, 500); } }

创建、更新和过期

在以下场景下,开发者可以指定要使用的数据变换和元素过渡方式:

  • 新数据点出现的时候(创建时)
  • 现有的数据发生变化(更新时)
  • 数据不再使用时(过期时)
const render = function(data, time) {
  // 渲染 'time'
  d3.select('.time')
    .text(moment(time).format("hh:mm a"))

  // 创建 selection,传入数据集
  const flight = d3.select('#chart')
    .selectAll('g.flight')
    .data(data, (d)=> d.id) // 为新的数据创建节点 const newFlight = flight.enter() .append("g") .attr('class', 'flight') const xPoint = (d)=> d.xScale(time);
  const yPoint = (d, i)=> 100 + i * 25;

  newFlight.append("circle")
    .attr('class',"flight-dot")
    .attr('cx', xPoint)
    .attr('cy', yPoint)
    .attr('r', "5")

  // 通过 select 更新现有的数据
  flight.select('.flight-dot')
    .attr('cx', xPoint)
    .attr('cy', yPoint)

  // 移除不需要的数据
  const oldFlight = flight.exit()
    .remove()
}

过渡

上述的代码实现了每 500ms 以 5 分钟为增量渲染画面:

  • 更新了时间;
  • 每当有新的航班图时,以圆为标识创建;
  • 更新当前航班的 x/y 坐标轴;
  • 当飞机到达目的地时,移除数据。

可以说已经实现了我们开始时的目标,但是每次新数据的出现和旧数据的销毁都太粗暴。通过在 D3 selection 上添加过渡,就能把这个过程变得平滑。

比如,为新数据添加到 DOM 结构中,通过改变透明度 (opacity),加一个渐现动画:

const newFlight = flight.enter()
  .append("g")
  .attr('class', 'flight')
  .attr('opacity', 0)

newFlight.transition()
  .duration(500)
  .attr('opacity', 1)

数据移除可以加上渐隐动画:

flight.exit()
  .transition()
  .duration(500)
  .attr('opacity', 0)
  .remove()

在横纵坐标点上也可以加上:

flight.select('.flight-dot')
  .transition()
  .duration(500)
  .ease('linear')
  .attr('cx', xPoint)
  .attr('cy', yPoint)

我们也能对这 5 分钟的增量做过渡,用 tween 函数实现每分钟时间都将出现,而不是每 5 分钟才会出现。

t 是用于变换的一个介于 0 到 1 的增长值。

const inFiveMinutes = moment(time).add(5, 'minutes').toDate();
const i = d3.interpolate(time, inFiveMinutes);
d3.select('.time')
  .transition()
  .duration(500)
  .ease('linear')
  .tween("text", ()=> { return function(t) { this.textCOntent= moment(i(t)).format("hh:mm a"); }; });

做个有创造力的人

D3 能做的事太多,虽然很想挖地更深一点,但我还是不在这本末倒置地一一讲解了。

  • 极地时钟
  • Force Layouts
  • 世界地图 & 3D 地球
  • Voronoi Tessellation
  • Prim’s Algorithm V
  • OMG Particles II

在 D3 Gallery 中你能找到更多例子。我强烈推荐 Scott Murray 的 D3 教程和D3 的官方文档。

希望读完这篇文章后你大概了解了要怎么使用 Selections、Scales 和 Transitions,能思索出独一无二的能极好地可视化数据的方案,实现之后可别忘了在评论区告诉我哦!

译文链接:实例教学:使用 D3.js 实现数据可视化


推荐阅读
  • 知识图谱——机器大脑中的知识库
    本文介绍了知识图谱在机器大脑中的应用,以及搜索引擎在知识图谱方面的发展。以谷歌知识图谱为例,说明了知识图谱的智能化特点。通过搜索引擎用户可以获取更加智能化的答案,如搜索关键词"Marie Curie",会得到居里夫人的详细信息以及与之相关的历史人物。知识图谱的出现引起了搜索引擎行业的变革,不仅美国的微软必应,中国的百度、搜狗等搜索引擎公司也纷纷推出了自己的知识图谱。 ... [详细]
  • 本文介绍了前端人员必须知道的三个问题,即前端都做哪些事、前端都需要哪些技术,以及前端的发展阶段。初级阶段包括HTML、CSS、JavaScript和jQuery的基础知识。进阶阶段涵盖了面向对象编程、响应式设计、Ajax、HTML5等新兴技术。高级阶段包括架构基础、模块化开发、预编译和前沿规范等内容。此外,还介绍了一些后端服务,如Node.js。 ... [详细]
  • Webpack5内置处理图片资源的配置方法
    本文介绍了在Webpack5中处理图片资源的配置方法。在Webpack4中,我们需要使用file-loader和url-loader来处理图片资源,但是在Webpack5中,这两个Loader的功能已经被内置到Webpack中,我们只需要简单配置即可实现图片资源的处理。本文还介绍了一些常用的配置方法,如匹配不同类型的图片文件、设置输出路径等。通过本文的学习,读者可以快速掌握Webpack5处理图片资源的方法。 ... [详细]
  • CSS3选择器的使用方法详解,提高Web开发效率和精准度
    本文详细介绍了CSS3新增的选择器方法,包括属性选择器的使用。通过CSS3选择器,可以提高Web开发的效率和精准度,使得查找元素更加方便和快捷。同时,本文还对属性选择器的各种用法进行了详细解释,并给出了相应的代码示例。通过学习本文,读者可以更好地掌握CSS3选择器的使用方法,提升自己的Web开发能力。 ... [详细]
  • 本文讨论了如何优化解决hdu 1003 java题目的动态规划方法,通过分析加法规则和最大和的性质,提出了一种优化的思路。具体方法是,当从1加到n为负时,即sum(1,n)sum(n,s),可以继续加法计算。同时,还考虑了两种特殊情况:都是负数的情况和有0的情况。最后,通过使用Scanner类来获取输入数据。 ... [详细]
  • Java验证码——kaptcha的使用配置及样式
    本文介绍了如何使用kaptcha库来实现Java验证码的配置和样式设置,包括pom.xml的依赖配置和web.xml中servlet的配置。 ... [详细]
  • 在project.properties添加#Projecttarget.targetandroid-19android.library.reference.1..Sliding ... [详细]
  • Html5-Canvas实现简易的抽奖转盘效果
    本文介绍了如何使用Html5和Canvas标签来实现简易的抽奖转盘效果,同时使用了jQueryRotate.js旋转插件。文章中给出了主要的html和css代码,并展示了实现的基本效果。 ... [详细]
  • 本文介绍了腾讯最近开源的BERT推理模型TurboTransformers,该模型在推理速度上比PyTorch快1~4倍。TurboTransformers采用了分层设计的思想,通过简化问题和加速开发,实现了快速推理能力。同时,文章还探讨了PyTorch在中间层延迟和深度神经网络中存在的问题,并提出了合并计算的解决方案。 ... [详细]
  • 本文介绍了绕过WAF的XSS检测机制的方法,包括确定payload结构、测试和混淆。同时提出了一种构建XSS payload的方法,该payload与安全机制使用的正则表达式不匹配。通过清理用户输入、转义输出、使用文档对象模型(DOM)接收器和源、实施适当的跨域资源共享(CORS)策略和其他安全策略,可以有效阻止XSS漏洞。但是,WAF或自定义过滤器仍然被广泛使用来增加安全性。本文的方法可以绕过这种安全机制,构建与正则表达式不匹配的XSS payload。 ... [详细]
  • 本文介绍了使用AJAX的POST请求实现数据修改功能的方法。通过ajax-post技术,可以实现在输入某个id后,通过ajax技术调用post.jsp修改具有该id记录的姓名的值。文章还提到了AJAX的概念和作用,以及使用async参数和open()方法的注意事项。同时强调了不推荐使用async=false的情况,并解释了JavaScript等待服务器响应的机制。 ... [详细]
  • 本文介绍了通过ABAP开发往外网发邮件的需求,并提供了配置和代码整理的资料。其中包括了配置SAP邮件服务器的步骤和ABAP写发送邮件代码的过程。通过RZ10配置参数和icm/server_port_1的设定,可以实现向Sap User和外部邮件发送邮件的功能。希望对需要的开发人员有帮助。摘要长度:184字。 ... [详细]
  • 动态规划算法的基本步骤及最长递增子序列问题详解
    本文详细介绍了动态规划算法的基本步骤,包括划分阶段、选择状态、决策和状态转移方程,并以最长递增子序列问题为例进行了详细解析。动态规划算法的有效性依赖于问题本身所具有的最优子结构性质和子问题重叠性质。通过将子问题的解保存在一个表中,在以后尽可能多地利用这些子问题的解,从而提高算法的效率。 ... [详细]
  • 高质量SQL书写的30条建议
    本文提供了30条关于优化SQL的建议,包括避免使用select *,使用具体字段,以及使用limit 1等。这些建议是基于实际开发经验总结出来的,旨在帮助读者优化SQL查询。 ... [详细]
  • 本文介绍了指针的概念以及在函数调用时使用指针作为参数的情况。指针存放的是变量的地址,通过指针可以修改指针所指的变量的值。然而,如果想要修改指针的指向,就需要使用指针的引用。文章还通过一个简单的示例代码解释了指针的引用的使用方法,并思考了在修改指针的指向后,取指针的输出结果。 ... [详细]
author-avatar
俺是胖墩墩_499
这个家伙很懒,什么也没留下!
Tags | 热门标签
RankList | 热门文章
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有