视觉高级篇27 # 如何实现简单的3D可视化图表:GitHub贡献图表的3D可视化?
Posted 凯小默
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了视觉高级篇27 # 如何实现简单的3D可视化图表:GitHub贡献图表的3D可视化?相关的知识,希望对你有一定的参考价值。
说明
【跟月影学可视化】学习笔记。
第一步:准备要展现的数据
可以使用这个生成数据:https://github.com/sallar/github-contributions-api
这里直接使用月影大佬的github提交数据的数据即可
结构大致如下:
第二步:用 SpriteJS 渲染数据、完成绘图
SpriteJS 是跨平台的高性能图形系统,它能够支持web、node、桌面应用和小程序的图形绘制和实现各种动画效果。
特性:
- 像操作DOM对象一样操作画布上的图形元素
- WebGL2渲染
- 多图层处理图形、文本、图像渲染
- DOM事件代理、自定义事件派发
- 使用ES6+语法和面向对象编程
- OffscreenCanvas和Web Worker多线程渲染
- 结构化对象树,对d3引擎友好,能够无缝使用
- 服务端渲染
- Vue
注意:需要加入 3d 扩展库加载并渲染3D模型。
SpriteJS 的 3D 部分,它是基于 OGL 库实现的。SpriteJS 在 OGL 的基础上,对几何体元素进行了类似 DOM 元素的封装。这样创建几何体元素就可以像操作 DOM 一样方便,可以直接用 d3 库的 selection 子模块来进行操作。
1. 创建 Scene 对象
const container = document.getElementById('stage');
// 创建 Scene 对象
const scene = new Scene(
container,
displayRatio: 2,
);
2. 创建 Layer 对象
在 SpriteJS 中,一个 Layer 对象就对应于一个 Canvas 画布。
// 创建 Layer 对象:创建了一个 3D(WebGL)上下文的 Canvas 画布
const layer = scene.layer3d('fglayer',
ambientColor: [0.5, 0.5, 0.5, 1],
camera:
fov: 35, // 相机的视角设置为 35 度
,
);
// 相机坐标位置为(6, 6, 6)
layer.camera.attributes.pos = [6, 6, 6];
// 相机朝向坐标原点
layer.camera.lookAt([0, 0, 0]);
3. 将数据转换成柱状元素
这里借助 d3-selection
,d3 是一个数据驱动文档的模型,d3-selection
能够通过数据操作文档树,添加元素节点。
https://github.com/d3/d3/blob/main/API.md
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>如何实现简单的3D可视化图表:GitHub 贡献图表的3D可视化</title>
<style>
#stage
width: 840px;
height: 640px;
border: 1px dashed #fa8072;
</style>
</head>
<body>
<script src="https://d3js.org/d3.v5.js"></script>
<div id="stage"></div>
<script type="module">
import Scene from 'https://unpkg.com/spritejs/dist/spritejs.esm.js';
import Cube, Light, shaders from 'https://unpkg.com/sprite-extend-3d/dist/sprite-extend-3d.esm.js';
// 获取该日期之前大约一年的数据
let cache = null;
async function getData(toDate = new Date())
if(!cache)
// 先从 JSON 文件中读取数据并缓存起来
const data = await (await fetch('./assets/github_contributions_akira-cn.json')).json();
cache = data.contributions.map((o) =>
o.date = new Date(o.date.replace(/-/g, '/'));
return o;
);
// 要拿到 toData 日期之前大约一年的数据(52周)
let start = 0,
end = cache.length;
// 用二分法查找
while(start < end - 1)
const mid = Math.floor(0.5 * (start + end));
const date = cache[mid];
if(date <= toDate) end = mid;
else start = mid;
// 获得对应的一年左右的数据
let day;
if(end >= cache.length)
day = toDate.getDay();
else
const lastItem = cache[end];
day = lastItem.date.getDay();
// 根据当前星期几,再往前拿52周的数据
const len = 7 * 52 + day + 1;
const ret = cache.slice(end, end + len);
if(ret.length < len)
// 日期超过了数据范围,补齐数据
const pad = new Array(len - ret.length).fill(count: 0, color: '#ebedf0');
ret.push(...pad);
return ret;
(async function ()
const container = document.getElementById('stage');
// 创建 Scene 对象
const scene = new Scene(
container,
displayRatio: 2, // 设置显示分辨率
);
// 创建 Layer 对象:创建了一个 3D(WebGL)上下文的 Canvas 画布
const layer = scene.layer3d('fglayer',
ambientColor: [0.5, 0.5, 0.5, 1],
camera:
fov: 35, // 相机的视角设置为 35 度
,
);
// 相机坐标位置为(6, 6, 6)
layer.camera.attributes.pos = [6, 6, 6];
// 相机朝向坐标原点
layer.camera.lookAt([0, 0, 0]);
// 创建用来 3D 展示的 WebGL 程序:shaders.GEOMETRY 默认支持 phong 反射模型的一组着色器
const program = layer.createProgram(
vertex: shaders.GEOMETRY.vertex,
fragment: shaders.GEOMETRY.fragment,
);
// 获取数据
const dataset = await getData(new Date(2019, 12, 31));
const max = d3.max(dataset, (a) =>
return a.count;
);
// 用数据来操作文档树
const selection = d3.select(layer);
/**
* 设置长方体 Cube 的属性
* 长 (width)
* 宽 (depth)
* 高 (height)
* y 轴的缩放 (scaleY):设置为 d.count 与 max 的比值,值在 0~1 之间
* 位置 (pos)坐标:根据数据的索引设置 x 和 z 来决定的。
* 长方体的颜色 (colors)
* */
const chart = selection.selectAll('cube')
.data(dataset)
.enter()
.append(() =>
return new Cube(program);
)
.attr('width', 0.14)
.attr('depth', 0.14)
.attr('height', 1)
.attr('scaleY', (d) =>
// max 是指一年的提交记录中,提交代码最多那天的数值。
return d.count / max;
)
.attr('pos', (d, i) =>
const x0 = -3.8 + 0.0717 + 0.0015;
const z0 = -0.5 + 0.05 + 0.0015;
const x = x0 + 0.143 * Math.floor(i / 7);
const z = z0 + 0.143 * (i % 7);
return [x, 0.5 * d.count /max, z];
)
.attr('colors', (d, i) =>
return d.color;
);
layer.setOrbit();
());
</script>
</body>
</html>
效果如下:
第三步:补充细节,实现更好的视觉效果
- 给柱状图添加光照
- 给柱状图增加一个底座
- 增加一个过渡动画,让柱状图的高度从不显示,到慢慢显示出来。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>如何实现简单的3D可视化图表:GitHub 贡献图表的3D可视化</title>
<style>
#stage
width: 840px;
height: 640px;
border: 1px dashed #fa8072;
</style>
</head>
<body>
<script src="https://d3js.org/d3.v5.js"></script>
<div id="stage"></div>
<script type="module">
import Scene from 'https://unpkg.com/spritejs/dist/spritejs.esm.js';
import Cube, Light, shaders from 'https://unpkg.com/sprite-extend-3d/dist/sprite-extend-3d.esm.js';
// 获取该日期之前大约一年的数据
let cache = null;
async function getData(toDate = new Date())
if(!cache)
// 先从 JSON 文件中读取数据并缓存起来
const data = await (await fetch('./assets/github_contributions_akira-cn.json')).json();
cache = data.contributions.map((o) =>
o.date = new Date(o.date.replace(/-/g, '/'));
return o;
);
// 要拿到 toData 日期之前大约一年的数据(52周)
let start = 0,
end = cache.length;
// 用二分法查找
while(start < end - 1)
const mid = Math.floor(0.5 * (start + end));
const date = cache[mid];
if(date <= toDate) end = mid;
else start = mid;
// 获得对应的一年左右的数据
let day;
if(end >= cache.length)
day = toDate.getDay();
else
const lastItem = cache[end];
day = lastItem.date.getDay();
// 根据当前星期几,再往前拿52周的数据
const len = 7 * 52 + day + 1;
const ret = cache.slice(end, end + len);
if(ret.length < len)
// 日期超过了数据范围,补齐数据
const pad = new Array(len - ret.length).fill(count: 0, color: '#ebedf0');
ret.push(...pad);
return ret;
(async function ()
const container = document.getElementById('stage');
// 创建 Scene 对象
const scene = new Scene(
container,
displayRatio: 2, // 设置显示分辨率
);
// 创建 Layer 对象:创建了一个 3D(WebGL)上下文的 Canvas 画布
const layer = scene.layer3d('fglayer',
ambientColor: [0.5, 0.5, 0.5, 1], // 环境光
camera:
fov: 35, // 相机的视角设置为 35 度
,
);
// 相机坐标位置为(6, 6, 6)
layer.camera.attributes.pos = [6, 6, 6];
// 相机朝向坐标原点
layer.camera.lookAt([0, 0, 0]);
// 添加一道白色的平行光,方向是 (-3, -3, -1)
const light = new Light(
direction: [-3, -3, -1],
color: [1, 1, 1, 1]
);
layer.addLight(light);
// 创建用来 3D 展示的 WebGL 程序:shaders.GEOMETRY 默认支持 phong 反射模型的一组着色器
const program = layer.createProgram(
vertex: shaders.GEOMETRY.vertex,
fragment: shaders.GEOMETRY.fragment,
);
// 获取数据
const dataset = await getData(new Date(2019, 12, 31));
const max = d3.max(dataset, (a) =>
return a.count;
);
// 用数据来操作文档树
const selection = d3.select(layer);
/**
* 设置长方体 Cube 的属性
* 长 (width)
* 宽 (depth)
* 高 (height)
* y 轴的缩放 (scaleY):设置为 d.count 与 max 的比值,值在 0~1 之间
* 位置 (pos)坐标:根据数据的索引设置 x 和 z 来决定的。
* 长方体的颜色 (colors)
* */
const chart = selection.selectAll('cube')
.data(dataset)
以上是关于视觉高级篇27 # 如何实现简单的3D可视化图表:GitHub贡献图表的3D可视化?的主要内容,如果未能解决你的问题,请参考以下文章