语雀
作品介绍
本次要分享的是使用 G2Plot 来实现「财新网」 诺贝尔奖可视化作品,原作品地址。本次对该作品进行了部分简化和改造,下文先介绍下具体的视觉设计和视觉元素对应的含义,再给大家介绍下实现该作品的一个技术实现思路。知乎视频www.zhihu.com
视觉设计
数据分析思路
(1)以时间线为导线,将获奖年龄划分为4个年龄阶段,按年份累计分别展示:各奖项获奖比例(内环图)
各国各奖项按获奖年龄分布的情况(环形散点图)
(2)最后对 1901-2015 年的获奖情况进行一个汇总,可以看到 42 个国家按照获奖年龄段在不同学科奖项上的分布情况,点越大代表获奖人数越多。
视觉通道映射
(1)获奖年龄:位置
(2)国家:位置
(3)奖项学科类别:颜色
(4)获奖人数:大小
(5)其他的数据信息:边栏
实现思路
初步了解视觉设计之后,我们来对该可视化信息图进行拆解分析,可以划分为以下几部分:内环图(含义:代表各奖项获奖比例;视觉通道:颜色;映射属性:奖项学科类别)+ 中心文本
环形散点图(含义:代表各国各奖项按获奖年龄分布的情况;视觉通道:位置、颜色、大小;分别映射属性:国家、获奖年龄、奖项学科类别、获奖人数)
辅助元素:年份边栏、当前年份滑块
经过拆解分析之后,我们可以把每一部分都作为一个 View(1)(即图层),对每一个 view 进行组装处理之后,进行叠加就可以形成一个多图层的信息图表了
G2Plot 多图层实验室透传 80% G2 能力,下面我们来看下如何利用 G2Plot 多图层实验室来实现该可视化信息图表,重点的部分会使用 ✨ 来标识 (*╹▽╹*)
内环图
我们先准备第一个 view
① 准备数据
② 设置坐标系:theta 坐标系,调整下半径 radius 和内半径 innerRadius
③ 绘制几何图形,确定视觉通道映射:环图对应的几何标记是 interval,x 轴无映射,y 轴位置映射到获奖数量,以 奖项类别 作为颜色映射,因此 type = interval yField = counts colorField = type ,同时还需要对数据进行调整,设置 adjust = stack
④ 进行图表组件和样式的装饰:通过 annotations 来配置中心文本标注,详细可见:Annotations 文档
伪代码:
const view1 = {
// ① 环图的数据 donutData
data: donutData,
region: { start: { x: 0, y: 0.35 }, end: { x: 1, y: 0.65 } },
coordinate: {
type: "theta",
cfg: { innerRadius: 0.84, radius: 0.96 },
},
geometries: [
{
type: "interval",
yField: "counts",
colorField: "type",
// 通道映射支持 color,size、shape 等
mapping: {
// 在 color 映射的回调中,我们可以进行颜色的自定义
color: ({ type }) => {
const idx = types.indexOf(type);
const { colors10 = [] } = labChart.chart.getTheme();
return colors10[idx] || "#D9D9D9";
},
},
adjust: { type: "stack" },
},
],
annotations: [
{
type: "text",
content: "G2Plot",
position: ["50%", "50%"],
style: {
textAlign: "center",
fontWeight: 400,
fontSize: 28,
},
},
],
};
效果图:
环形散点图
① 准备数据
② 设置坐标系:polar 极坐标系,调整下半径 radius 和内半径 innerRadius
③ 绘制几何图形,确定视觉通道映射:散点图对应的几何标记是 point,x 轴映射到国家,y 轴位置映射到获奖年龄,以 奖项类别 作为颜色映射,因此 type = point xField = country yField = ageGroup colorField = type ,同时还需要对数据进行调整,设置 adjust = dodge (这里我们对同颜色的 point 进行分组错开)
④ 进行图表组件和样式的装饰:可以适当调整下坐标轴 axes 的样式,详细可见:Axis 文档
⑤ ✨ 放射状的国家标签 ✨:这里我们可以巧妙利用 polar 坐标系下的 label 设置 labelEmit 来达到一个放射状标签的效果。新增一个 interval 的几何标记,x 轴映射到国家,y轴无需映射或者映射到 "1",详细可见:Label 文档
const view2 = {
// ① 散点图的数据 pointViewData
data: pointViewData,
region: { start: { x: 0, y: 0 }, end: { x: 1, y: 1 } },
coordinate: {
type: "polar",
cfg: {
innerRadius: 0.45,
radius: 0.64,
},
},
axes: {/* 略 */},
geometries: [
{
type: "point",
xField: "country",
yField: "ageGroup",
colorField: "type",
sizeField: "number",
adjust: {
type: "dodge",
},
mapping: {
size: [2, 8],
shape: "circle",
style: {
fillOpacity: 0.65,
lineWidth: 0,
},
},
},
{
// 国家标签
type: "interval",
xField: "country",
label: {
labelEmit: true,
fields: ["country"],
offset: 50,
style: {
fontSize: 10,
},
},
mapping: {
color: "transparent",
},
},
],
};
效果图:
辅助元素:年份边栏、当前年份滑块
最后我们还剩下一些辅助元素,我们继续准备一个 view,主要实现一个年份的边栏和当前年份的滑块。同样我们还是可以借助 polar 坐标系下的 label 设置 labelEmit 来达到一个放射状标签的效果。数据就是 1901 - 2015年啦(这里附加2条数据:1900和2016,作为起点和重点)
确定视觉通道映射:几何标记选择 line 或 interval 均可,因为没有实际含义,只是需要用到 label 标签的展示;x 轴映射 year,y 轴无映射,label 设置 labelEmit ;边栏和滑块使用不同的几何标记(geometry),对应使用不同的 label 组件,便于后期的交互处理。
确定好思路之后,代码大概如下:
const view3 = {
// ① 年份数据 pointViewData
data: yearData,
region: { start: { x: 0.05, y: 0.05 }, end: { x: 0.95, y: 0.95 } },
axes: {/* 略 */},
coordinate: { type: "polar", cfg: { innerRadius: 0.99, radius: 1 } },
geometries: [
{
type: "line",
xField: "year",
label: {
labelEmit: true,
content: ({ year }) => {/* ✨ 重点处理,通过回调只展示整数值的年份 */},
},
mapping: {
// 几何图形没有实际含义,设置填充色为 透明
color: "transparent",
},
},
{
type: "interval",
xField: "year",
label: {
labelEmit: true,
fields: ["year"],
callback: (year) => {
return {/* ✨ 重点处理,通过回调只展示当前年份 */},
},
mapping: {
// 几何图形没有实际含义,设置填充色为 透明
color: "transparent",
},
},
]
};
整合到多图层实验室(MultiView)MultiView 的每个图层包括有自己的:数据、图形、图形映射,可以独立完成自己的逻辑组装。
tooltip、legend 等在顶层进行配置。如下:设置 legend: { number: false } 可以将 number 字段映射的图例进行关闭,其他同普通 plot 的图例和 tooltip 使用方式保持一致,保持心智统一,带来更加友好的开发体验。
高级进阶:动态交互
从前面的预览图,可以看到有一些动态交互:① 整体按照时间线进行轮播变换;② 单击边栏,将滑块移动至指定年份;③ 双击边栏,将滑块移动至指定年份,并且停止轮播
整体按照时间线进行轮播变换
可以看到轮播的时候,变化的是「内环图」和「散点图」的数据,因此我们可以启动一个定时器来定时改变这两个 view 对应的数据。
✨ 技术实现关键点:获取「内环图」和「散点图」对应的 view,通过 labChart.chart 获取到 G2 的 chart 对象,再通过 chart.views 可获取到所有的 view 对象,再根据 views 传进的顺序即可获取到对应的 view
获取「内环图」和「散点图」当前年份对应的数据,调用 view.changeData
function rerender(specYear) {
labChart.chart.views[0].changeData(getIntervalViewData(specYear));
labChart.chart.views[1].changeData(getPointViewData(specYear));
}
单击边栏,将滑块移动至指定年份
关于这个交互要做的事情:监听边栏元素点击事件,将当前年份修改为鼠标事件指定年份,并且移动滑块和重新渲染环图与散点图。
✨ 技术实现关键点:监听边栏元素点击事件,获取边栏对应的 view 对象,监听图形元素(2)(element)点击事件
高亮当前选中年份,进行滑块移动(这里通过指令式的方式,重新调用 label 通道,再执行 render(true) 进行重新绘制,而非重新渲染)
const view1 = labChart.chart.views[0];
const view2 = labChart.chart.views[1];
const view3 = labChart.chart.views[2];
// 根据 view3 中创建的顺序,可知 滑块对应的第 2 个几何标记对象 geometryconst sliderBlock = view3.geometries[1];
function rerender(specYear) {
view1.changeData(getIntervalViewData(specYear));
view2.changeData(getPointViewData(specYear));
sliderBlock.label('year', (year) => {
const { defaultColor } = labChart.chart.getTheme();
return {
labelEmit: true,
style: {
fill: year === specYear ? 'rgba(255,255,255,0.85)' : 'transparent',
},
content: () => `${specYear === 2016 ? ' ALL ' : specYear}`,
background: {
padding: 2,
style: {
radius: 1,
// 非当前年份,进行透明展示 fill: year === specYear ? defaultColor : 'transparent',
},
},
};
});
// 传入参数 true,重新绘制,不重新触发更新流程。 view3.render(true);
}
// 监听 element click 事件,指定当前年份,并且启动轮播view3.on('element:click', (evt) => {
handldSlideBlockClick(evt);
start();
});
// 监听 element click 事件,指定当前年份,并且暂停轮播view3.on('element:dblclick', (evt) => {
handldSlideBlockClick(evt);
end();
});双击边栏,将滑块移动至指定年份,并且停止轮播同理,只要将监听事件修改为 dblclick 再执行具体的事件处理即可
最后
这周分享的可视化信息图表实现原理就到这里了,源代码在 G2Plot 的 官网示例 上,大家可以亲自动手尝试下~
更多文章,可以帮助了解 G2 的基础概念和进阶内容:
View(图层)
View 是图层容器的概念,每一个 View 拥有自己独立的数据源、坐标系、几何标记、Tooltip 以及图例,可以理解 View 是用来组装数据,Component,Geometry 的容器。 一个 View 可以包含有多个子 View,通过这种嵌套关系,可以将一个画布按照不同的布局划分多个不同区域(分面),也可以将不同数据源的多个 View 叠加到一起,形成一个多数据源,多图层的图表。详细文档
Geometry(几何标记)
geometry 即你在图表中实际看到的图形元素,如点、线、多边形等,每个几何标记对象含有多个图形属性,G2 的核心就是建立数据中的一系列变量到图形属性的映射。详细文档。
Element(图形元素)
Element 即一条/一组数据对应的图形元素,它代表一条数据或者一个数据集。
数据在不同的 geometry 上的渲染方式不同,在点图上一条数据会对应一个点,在柱状图中 每条数据对应一个柱子,而在折线图上则是一组数据对应一条折线。
职责:绘制、更新、销毁 Shape & 状态管理