# 37 | 实战(一):如何使用图表库绘制常用数据图表? 你好,我是月影。 图表是我们在可视化中展示数据常用的方式之一,常见的有柱状图、折线图、饼图等等,我们之前也都一一实现过。如果我让你总结一下图表的实现方法,你能说出来吗?总结不出来也没关系,这节课,我们就一起梳理一下实际项目中常用的制图方法。 实现图表有两种方式,一是使用现成的图表库,二是使用数据驱动框架,前者胜在简单易用,后者则更加灵活。所以,我们会用两节课的时间分别来讲,使用图表库和使用数据驱动框架的制图思路。 因为使用图表库更加简单,所以这一节课我们先来讲怎么使用它实现图表。另外,这节课的实践性比较强,我建议你跟着我的讲解一块儿动手去写,这样能更快地理解课程的内容。 ## 课前准备 说了这么多,我们今天到底会用哪些图表库呢?我们主要会使用我们比较熟悉的SpriteJS和QCharts来绘制图表。其他的图表库,例如更常用的[ECharts](https://echarts.apache.org/),它在图表实现上的原理和用法和我们今天讲的基本相同,所以学完了今天的内容,你完全可以根据它的教程文档去自学。 好了,确定了要使用的工具之后,我们就要配置和加载SpriteJS和QCharts。具体怎么做呢? 最简单的方式是,我们直接通过CDN,用script标签来加载SpriteJS和QCharts打包好的文件。 ``` ``` 如果是完整的工程项目的话,你也可以使用webpack来打包和加载模块。这里有一个[Quick Start](https://github.com/qcharts/quickstart),你可以fork这个项目,按照其中的配置项来设置。当加载完成之后,我们就可以开始绘制基础的图表了。 ## QCharts图表的基本用法 QCharts图表由图表(Chart)对象及其子元素构成。其中图表对象的子元素包含图形(Visual)和其他插件(Plugin)。图形是必选元素,其他的插件都是可选元素。 图表在构建的时候,需要传入一个DOM元素,这个元素可以是页面上任意一个块级元素,QCharts用这个元素作为容器来创建Canvas画布,并其还会根据容器的大小来设置Canvas画布的大小。默认情况下,图表会根据画布大小来自动适配。 接下来,我们看一下具体的操作。首先,我们创建一个图表对象,设置它的容器设置成一个id为app的元素。 ``` const { Chart, Line } = qcharts; const chart = new Chart({ container: '#app' }); ``` 创建了这个容器之后,我们就可以为它绑定数据,假设我们绑定一份销售数据。 ``` const data = [ { date: '05-01', category: '图例一', sales: 15.2 }, { date: '05-02', category: '图例一', sales: 39.2 }, { date: '05-03', category: '图例一', sales: 31.2 }, { date: '05-04', category: '图例一', sales: 65.2 }, { date: '05-05', category: '图例一', sales: 55.2 }, { date: '05-06', category: '图例一', sales: 75.2 }, { date: '05-07', category: '图例一', sales: 95.2 }, { date: '05-08', category: '图例一', sales: 100 } ]; chart.source(data, { row: 'category', value: 'sales', text: 'date' }); ``` 如上面代码所示,我们将数据内容与图表对象通过chart.source方法绑定起来。绑定数据的时候,我们可以指定数据的行(列)、数值和文本,这些设置会被图形(Visual)和其他插件使用。这里,我们将行设为category这个字段,数值设为sales字段,文本设为data字段。 设置之后,图表并没有马上显示出来。这是因为,我们还没有给图表指定图形。QCharts支持多种图形对象,比如Line、Area、Bar等等。假设我们选择了Line对象,那我们只需要创建它,然后将它添加到chart对象的子元素中就可以了。 ``` const line = new Line(); chart.append([line]); ``` 这样,我们就让图形显示出来了,效果如下: ![](https://static001.geekbang.org/resource/image/b6/9f/b6a87f9984082b6a02a99b484262ef9f.jpeg?wh=1920*1080) 了解了QCharts图表的基本用法之后,我们一起进入实践环节吧。 ## QCharts绘制折线图、面积图、柱状图和饼图的方法 我们先来讲讲折线图、面积图、柱状图和饼图的实现方法,因为之前已经实现过很多次了,所以理解起来比较容易。 首先是折线图,它是可视化中最常用的图表之一。刚才我们用Line图形绘制了一条折线,但它还不是完整的折线图。完整的折线图,除了有图形以外,还要有表示数据的坐标轴、提示信息和图例,这些都需要在QCharts中由插件来完成。 接下来,我们就继续给前面的代码添加元素。首先,我们给它增加两个坐标轴,分别是底部和左侧的。 我们通过Axis类来创建axis对象,默认的坐标轴是在底部不用修改,再通过.style改变它的样式属性,比如我们将"grid"设置为false,这样画布上不会显示纵向的网格线。 ``` const axisBottom = new Axis().style("grid", false); ``` 然后,我们同样创建一个axis对象,通过设置orient: “left” 让它朝左,这样就成功创建了左侧的坐标轴。同样,我们也要把它样式中的"axis"属性设置为false,这样画布上就不会显示坐标轴的实线了。 ``` const axisLeft = new Axis({ orient: "left" }) .style("axis", false); ``` 最后,我们将两个坐标轴添加到chart对象的子元素中去,就可以将坐标轴显示出来。 ![](https://static001.geekbang.org/resource/image/62/84/62c0556935f2756c6eac5a88057eb784.jpeg?wh=1920*1080) 添加完坐标轴之后,我们再添加图例(Legend)和提示(Tooltip)。最简单的方式,是我们直接创建两个对象,然后将它们添加到charts对象的子元素中。 ``` const legend = new Legend(); const tooltip = new Tooltip(); chart.append([line, axisBottom, axisLeft, legend, tooltip]); ``` 添加了图例和提示信息后的图表如下图所示: ![](https://static001.geekbang.org/resource/image/58/85/5896a90b8979ecaf081726195f632985.jpeg?wh=1920*1080) **接下来,我们再来说说怎么用QCharts绘制面积图和柱状图。** 学会了折线图的绘制方法,我们就可以用相同的思路非常简单地绘制其他图形了,比如说,我们可以简单将Line对象换成Area或Bar对象,这样就可以用同样的数据绘制出面积图或柱状图。这是为什么呢? ![](https://static001.geekbang.org/resource/image/2b/bf/2b29397b83cdbe7630bd88982d717cbf.jpeg?wh=1920*1080 "将 Line 改成 Area 绘制出面积图") ![](https://static001.geekbang.org/resource/image/c7/fd/c70fe007a4e74124a8e23afbb59175fd.jpeg?wh=1920*1080 "将 Line 改成 Area 绘制出柱状图") 因为像折线图、面积图、柱状图这些表现形式不同的图表,它们能够接受同样格式的数据,只是想要侧重表达的信息不同而已。一般来说,折线图强调数据变化趋势,柱状图强调数据的量和差值,而面积图同时强调数据量和变化趋势。在实际项目中,我们要根据不同的需求选择不同的基本图形。 如果要强调整体和局部比例,我们还可以选择绘制饼图。与折线图、面积图、柱状图相比,饼图不用配置图例,因为图例会自动生成,而且饼图也不需要坐标轴,所以使用起来更加简单。 ``` const data = [ { date: '05-01', sales: 15.2 }, { date: '05-02', sales: 39.2 }, { date: '05-03', sales: 31.2 }, { date: '05-04', sales: 65.2 }, { date: '05-05', sales: 55.2 }, { date: '05-06', sales: 75.2 }, { date: '05-07', sales: 95.2 }, { date: '05-08', sales: 100 } ]; chart.source(data, { row: 'date', value: 'sales', text: 'date' }); const pie = new Pie(); const legend = new Legend(); const tooltip = new Tooltip(); chart.append([pie, legend, tooltip]); ``` 上面的代码用同样的数据绘制了一个饼图。 ![](https://static001.geekbang.org/resource/image/12/7d/12dffaec3578995169de1db76491947d.jpeg?wh=1920*1080) ## QCharts绘制雷达图、仪表盘和玉玦图、南丁格尔玫瑰图 讲完了这些常见的基础图表,我们再来看几个比较有趣的图表。 首先是雷达图,它一般用来绘制多组固定数量的数据,可以比较直观地显示出这组数据的特点。我们经常会在游戏中看见它的应用,比如,下面这张图表就显示了三国武将诸葛亮的各方面数据。 ![](https://static001.geekbang.org/resource/image/96/ae/96b37e3a3267470ce6a95e3c358dcbae.jpeg?wh=1920*1080) 雷达图的实现代码如下: ``` const data = [ { date: '体力', category: '诸葛亮', sales: 100 }, { date: '武力', category: '诸葛亮', sales: 69 }, { date: '智力', category: '诸葛亮', sales: 100 }, { date: '统帅', category: '诸葛亮', sales: 95 }, { date: '魅力', category: '诸葛亮', sales: 95 }, { date: '忠诚', category: '诸葛亮', sales: 100 }, ]; chart.source(data, { row: 'category', value: 'sales', text: 'date' }); const radar = new Radar(); radar.style('section', (d) => ({ opacity: 0.3 })); const legend = new Legend({ align: ['center', 'bottom'] }); const tooltip = new Tooltip(); chart.append([radar, legend, tooltip]); ``` 除此之外,还有一些其他类型的图表,可以用来展示特殊的信息。比较典型的有仪表盘,它可以显示某个变量的进度。 ![](https://static001.geekbang.org/resource/image/6a/9e/6a21ab85d811fa8a6abyy30ab0516b9e.jpeg?wh=1920*1080) 仪表盘实现代码比较简单,如下所示: ``` const gauge = new Gauge({ min: 0, max: 100, percent:60, lineWidth: 20, tickStep: 10 }); gauge.style('title', { fontSize: 36 }); chart.append(gauge); ``` 如果我们要显示多个变量的进度,还可以使用玉玦图。 ![](https://static001.geekbang.org/resource/image/b7/82/b76efc4a8c052fb8d11ba976ef039782.jpeg?wh=1920*1080) 对应的代码也非常简单,如下所示: ``` const data = [ { type: '政法干部', count: 6654 },{ type: '平安志愿者', count: 5341 },{ type: '人民调解员', count: 3546 },{ type: '心理咨询师', count: 4321 },{ type: '法律工作者', count: 3923 },{ type: '网格员', count: 5345 } ].sort((a, b) => a.count - b.count); chart.source(data, { row: 'type', text: 'type', value: 'count' }); const radialBar = new RadialBar({ min: 0, max: 10000, radius: 0.6, innerRadius: 0.1, lineWidth: 10 }); radialBar.style('arc', { lineCap: 'round' }); const legend = new Legend({ orient: 'vertical', align: ['right', 'center'], }); chart.append([radialBar, legend, new Tooltip()]); ``` 最后是南丁格尔玫瑰图,它可以显示多维度的信息,比如下图就显示了男女游客在公园四个区域内的分布情况。南丁格尔玫瑰图的绘制思路和代码,我们在[第35节课](https://time.geekbang.org/column/article/288323)已经说过,这里就不再多说了。如果你还不熟悉,可以再去回顾一下。 ![](https://static001.geekbang.org/resource/image/47/a8/47eca31ddbf1cb2423aa3db23bcabca8.jpeg?wh=1920*1080) ## QCharts绘制图表组合 我们刚才讲的都是在图表上绘制单一变量,那要想在图表上聚合多元变量,比如在一张天气图表上同时展示降水量、气温,我们可以同时绘制多个图形来表示。 我们可以分两种情况来讨论,最简单的情况是用相同的图形来绘制不同的变量。这个时候,我们可以直接通过不同category来绘制多个图形。比如,下面的数据就是用两组category分别绘制了两条折线。 ``` const data = [ { date: "1月", catgory: "降水量", val: 15.2 }, { date: "2月", catgory: "降水量", val: 19.2 }, { date: "3月", catgory: "降水量", val: 11.2 }, { date: "4月", catgory: "降水量", val: 45.2 }, { date: "5月", catgory: "降水量", val: 55.2 }, { date: "6月", catgory: "降水量", val: 75.2 }, { date: "7月", catgory: "降水量", val: 95.2 }, { date: "8月", catgory: "降水量", val: 135.2 }, { date: "9月", catgory: "降水量", val: 162.2 }, { date: "10月", catgory: "降水量", val: 32.2 }, { date: "11月", catgory: "降水量", val: 32.2 }, { date: "12月", catgory: "降水量", val: 15.2 }, { date: "1月", catgory: "气温", val: 2.2 }, { date: "2月", catgory: "气温", val: 3.2 }, { date: "3月", catgory: "气温", val: 5.2 }, { date: "4月", catgory: "气温", val: 6.2 }, { date: "5月", catgory: "气温", val: 8.2 }, { date: "6月", catgory: "气温", val: 15.2 }, { date: "7月", catgory: "气温", val: 25.2 }, { date: "8月", catgory: "气温", val: 23.2 }, { date: "9月", catgory: "气温", val: 24.2 }, { date: "10月", catgory: "气温", val: 16.2 }, { date: "11月", catgory: "气温", val: 12.2 }, { date: "12月", catgory: "气温", val: 6.6 }, ]; chart.source(data, { row: "catgory", value: "val", text: "date", }); const line = new Line({ axisGap: true }); ... ``` ![](https://static001.geekbang.org/resource/image/b8/92/b88746440cae3937beb3c638b9bda392.jpeg?wh=1920*1080) 如果我们想用不同类型的图形来展示多个变量,在QCharts中,我们只需要创建多个不同的图形对象,然后把它们都添加到chart对象的子元素中去就可以了。 ``` const ds = chart.dataset; const d1 = ds.selectRows("降水量"); const line = new Line({ axisGap: true }) .source(d1) .style("point", { strokeColor: "#fff" }); const d2 = ds.selectRows("气温"); const bar = new Bar().source(d2); bar.style("pillar", { fillColor: "#6CD3FF" }); ``` 如上面代码所示,我们先可以通过chart.dataset拿到通过.source绑定给chart对象的数据集,然后,通过selectRows分别将降水量和气温数据过滤出来。接着,我们分别创建Line和Bar两个图形对象,再将降水量和气温数据分别绑定给它们。最后,我们将这两个对象同时添加到chart子元素列表里,就可以将两个不同类型的图形显示出来了,具体的效果如下: ![](https://static001.geekbang.org/resource/image/f8/62/f8688798100f4a9yyc04c9d34fc79262.jpeg?wh=1920*1080) ## 要点总结 这节课,我们主要学习了QCharts图表库的使用。 QCharts是基于SpriteJS的简单可视化图表库,我们通过它可以绘制各种类型的图表。一般来说,我们是先创建图表对象,然后绑定数据,接着添加图形对象以及其他的插件,包括图例和提示。通过将图形和插件以子元素的形式添加到图表对象上,就能把图表内容最终显示出来了。 我们可以很方便地根据数据特点和业务需要,用数据绘制折线图、面积图、柱状图、饼图、雷达图等图表,还可以绘制特殊的仪表盘和玉玦图。另外,如果要显示多维数据,我们也可以用稍微复杂一些的南丁格尔玫瑰图。 最后,我们还可以把多维度变量聚合在一个图表中来显示不同的图形组合。具体操作是,我们先筛选数据,然后创建不同类型的图形对象,最后将它们都添加到图表对象的子元素中。 总的来说,我们今天讲的其实都是QCharts图表库,最基础、最常用的方法。QCharts还提供了众多其他类型的图表,以及灵活操作图表样式的API。如果你有兴趣继续钻研,可以通过我课后给出的参考链接进一步学习。 ## 小试牛刀 你学会了使用不同图表来表达不同数据了吗?你可以试着使用GitHub仓库里的北京市天气数据和空气质量数据,实现一个温度、湿度、风速、空气质量的聚合图表吗?如果用来展示温度、湿度、风速和空气质量的图形都不相同就更好了。 这些常用的制图方法你都学会了吗?下节课,我们会接着来学,怎么使用数据驱动框架来表达不同数据,挑战才刚刚开始呢,我们下节课再见! * * * ## 源码 课程代码详见[GitHub仓库](https://github.com/akira-cn/graphics/tree/master/qcharts) ## 推荐阅读 [QCharts官网](https://www.qcharts.cn/#/home)