Github Repository 可视化 (D3.js & Three.js)
先上 Demo 链接 & 效果图
demo 链接
github 链接
效果图 2D:
效果图 3D:
为什么要做这样一个网站?
最初想法是因为 github 提供的页面无法一次看到用户的所有 repository, 也无法直观的看到每个 repository 的量级对比(如 commit 数, star 数),
所以希望做一个能直观展示用户所有 repository 的网站.
实现的功能有哪些?
用户 Github Repository 数据的2D和3D展示, 点击用户 github 关注用户的头像, 可以查看他人的 Github Repository 展示效果.
2D 和 3D 版本均支持:
- 展示用户的 Repository 可视化效果
- 点击 following people 的头像查看他人的 Repository 可视化效果
其中 2D 视图支持页面缩放和拖拽 && 单个 Repository 的缩放和拖拽, 3D 视图仅支持页面的缩放和拖拽.
用到了哪些技术?
- 数据来源为 Github 提供的 GraphQL API.
- 2D 实现使用到了 D3.js
- 3D 实现使用到了 Three.js
- 页面搭建使用 Vue.js
实现细节?
2D 实现
2D 效果图中, 每一个 Repository 用一个圆形表示, 圆形的大小代表了 commit 数目 || start 数目 || fork 数目.
布局使用的是 d3-layout 中的 forceLayout, 达到模拟物理碰撞的效果. 拖拽用到了 d3-drag 模块, 大致逻辑为:
==> 检测鼠标拖拽事件
==> 更新 UI 元素坐标
==> 重新计算布局坐标
==> 更新 UI 来达到圆形可拖拽的效果.
让我们来看看具体代码:
2D 页面依赖 D3.js 的 force-layout 进行动态更新, 我们为 force-layout 添加了以下几种 force(作用力):
.force('charge', this.$d3.forceManyBody())
添加节点之间的相互作用力.force('collide',radius)
添加物理碰撞, 半径设置为圆形的半径.force('forceX', this.$d3.forceX(this.width / 2).strength(0.05))
添加横坐标居中的作用力.force('forceY', this.$d3.forceY(this.height / 2).strength(0.05))
添加纵坐标居中的作用力
主要代码如下:
this.simulation = this.$d3
.forceSimulation(this.filteredRepositoryList)
.force('charge', this.$d3.forceManyBody())
.force(
'collide',
this.$d3.forceCollide().radius(d => this.areaScale(d.count) + 3)
)
.force('forceX', this.$d3.forceX(this.width / 2).strength(0.05))
.force('forceY', this.$d3.forceY(this.height / 2).strength(0.05))
.on('tick', tick)
最后一行 .on('tick', tick)
为 force-layout simulation 的回调方法, 该方法会在物理引擎更新的每个周期被调用, 我们可以在这个回调方法中更新页面, 以达到动画效果.
我们在这个 tick
回调中要完成的任务是: 刷新 svg 中 circle 和 html 的span 的坐标. 具体代码如下.
如果用过 D3.js 的同学应该很熟悉这段代码了, 就是使用 d3-selection 对 DOM 元素 enter(), update(), exit()
三种状态进行的简单控制.
这里需要注意的一点是, 我们没有使用 svg 的 text 元素来实现文字而是使用了 html 的 span, 目的是更好的控制文字换行.
const tick = function() {
const curTransform = self.$d3.zoomTransform(self.div)
self.updateTextLocation()
const texts = self.div.selectAll('span').data(self.filteredRepositoryList)
texts
.enter()
.append('span')
.merge(texts)
.text(d => d.name)
.style('font-size', d => self.textScale(d.count) + 'px')
.style(
'left',
d =>
d.x +
self.width / 2 -
((self.areaScale(d.count) * 1.5) / 2.0) * curTransform.k +
'px'
)
.style(
'top',
d => d.y - (self.textScale(d.count) / 2.0) * curTransform.k + 'px'
)
.style('width', d => self.areaScale(d.count) * 1.5 + 'px')
texts.exit().remove()
const repositoryCircles =