国庆节回老家,看到家族里长辈们整理的家谱并印成了一本书,到我这一代已经是第六代了。我辈分低,从小在村里见了跟我爸年龄差不多的人都是爷爷婆婆的叫,但对于这些族人的族内关系却不清楚——从我记事起就没见过很正式的家谱,据说早先有过,但文革时破四旧遗失了,后来再也没人整理。这次的家谱书比较清晰的记录了每一家在家族中的位置关系和家庭成员信息,对于家族关系用一个跨了四页的表格列出。我翻阅时想到如果在电脑上用图表中的树图把家族关系列出来会更清楚直观,但直到前几天结课才有时间动手。我是做web前端的,用比较熟悉的echarts树图尝试画了一下,经过两周业余时间的开发,实现初版计划的功能。用echarts实现家谱图的过程也遇到不少困难,我把一些解决问题的经验记录一下,供需要到的小伙伴们参考。在截图举例中我将家人的姓名都替换成“名字”二字,以保护姓名隐私。
完成后效果如图:
export const data = {
name: "祖父",
children: [{
name: "父亲",
value: 1,
children: [{
name: "我",
value: 1,
children: [{
name: "儿子1",
value: 3,
},{
name: "女儿",
value: 2,
}
…… // 其他孩子
],
},{
name: "兄弟",
value: 2,
}
…… // 其他兄弟姐妹
],
},{
name: "伯父",
value: 1,
}
…… // 伯伯、叔叔、姑姑
],
}
"dependencies": {
"echarts": "4",
"sass": "^1.52.3",
"react": "^17.0.1",
"react-dom": "^17.0.1",
"react-router-dom": "^5.2.0",
"react-scripts": "4.0.1",
"web-vitals": "^0.2.4"
},
import { data as baseData } from '../../data/base'
import echarts from 'echarts' // echarts5.0目前不受支持,只能用4.x
export default function Home() {
useEffect(() => {
// svg渲染性能对移动端更友好,且缩放不影响字体清晰度,但使用toolbox内置保存图片会得到svg格式。而截图时svg更清晰。
const mainChart = echarts.init(document.querySelector('.mainChart'), {}, {renderer: 'svg'})
mainChart.setOption({
series: [{
type: 'tree',
data: [baseData],
initialTreeDepth: -1, // 可以让所有节点都展开
orient: 'vertical', // 方向改为自上而下
}]
});
}, [])
return
}
//scss设置
.p-home {
min-width: 1600px;
height: 800px;
:global {
.mainChart {
height: 100%;
}
}
}
html中增加样式设置:
完成后得到如下图表,整体结构出来了,但主要问题是名字横排,在离得近的地方重叠了,如果再显示上排行,即使换行也会重叠很多。
由于echarts的树图label不支持文字竖排,我给提了issue(https://github.com/apache/incubator-echarts/issues/13845),等待以后某个版本作为新功能出现。但通过一些技巧,在现行版本上我们依然可以实现这个效果。我的做法是把需要竖排的两行文本穿插了再横排换行,比如名字“刀客”,排行老二,考虑到加上中文括号的效果,会拼接为第一行“ ︵”,注意第一个字符是个全角空格,普通空格在对齐上总不尽人意;第二行“刀老”,第三行“客二”,第四行“ ︶”。
const getChineseNum = num => {
const chineseNum = '〇一二三四五六七八九十'.split('')
if (num < 0) {
return '待定'
} else if (num === 1) {
return '老大'
} else if (num < 11) {
return '老' + chineseNum[num]
} else if (num % 10 === 0) {
return chineseNum[num / 10] + '十'
} else if (num < 20) {
return '十' + chineseNum[num % 10]
} else {
return chineseNum[Math.floor(num / 10)] + '十' + chineseNum[num % 10]
}
}
const getLabel = ({name, value, ranking}) => {
if (!value) { // 女性名字直接换行
return name.split('').join('\n')
}
ranking = '︵' + ranking + '︶'
name = ' ' + name
return ranking.split('').reduce((total, curr, index) => total + (name[index] || ' ') + curr + '\n', '')
}
label: {
fontSize: 11,
position: 'top',
formatter: params => {
const {name,value} = params
const ranking = getChineseNum(params.value)
return getLabel({name, value, ranking})
}
},
优化后效果如下
此时我们看到的家谱已经比较清晰了,然后我们用颜色区分男女,并让对应的节点为实心同色以增加区别度。
先定义方法formatData,以原始数据为准递归遍历,添加样式信息。此处根据男女名称的对齐情况分别设置位置,并根据显示规律,将同一节点下第一位男性向左偏移,最后一个如果是男性向右偏移,而女性全部向右偏移。男性名字统一位置下调。
const formatData = (obj, idx, arr) => {
Object.keys(obj).forEach(item => {
if (item === 'children' && obj.children.length) {
obj.children.forEach((item0, idx, arr) => formatData(item0, idx, arr))
} else {
if (item === 'value') {
if (obj.value === 0) { // 女
obj.label = {
color: 'crimson',
offset: [5, 3]
}
obj.itemStyle = {
color: 'crimson'
}
} else { // 男
let left = 0
if (!idx) {
left = -5 // children中第一个节点标签左偏移
}
if (idx === arr.length - 1) {
left = 5 // children中最后一个节点标签右偏移
}
obj.label = {
offset: [left, 20]
}
}
}
if (item === 'name' && obj.name.length === 3) { // 如果是3个字则不显示第一个,即去掉姓氏
obj.name = obj.name.replace('李', '')
}
}
})
return obj
}
设置默认label颜色为深蓝色
label: {
……
color: 'darkblue',
}
series中的数据修改,并增加一些属性以让节点样式和名字颜色统一,以及最大程度的利用空间
series:[{
……
left: '1.5%',
right: '1.5%',
top: '10%',
bottom: '5%',
data: [formatData(baseData)],
symbol: 'circle',
symbolSize: 8,
itemStyle: {
color: 'darkblue',
borderWidth: 0
},
}]
这一步完成后就得到了开篇时截图的样子,家谱图表基本完成。
因为这个家谱完成后主要通过发微信链接让大家来看,所以要考虑移动端展示的最优方案。如果直接输出网页,在微信浏览器中不能自动横屏,而这个家谱以横屏方式观看比较好,能充分利用空间。所以先检测设备的宽高,如果高度大于宽度则横着显示,即将图表旋转90度再平移到视图范围内。这个功能我写在app.js中。
function App() {
useEffect(() => {
const windowW = window.innerWidth // 这里获取的是物理像素,而控制台上获取的是css像素
const windowH = window.innerHeight
if (windowH > windowW) { // 竖屏
const root = document.querySelector('#root')
const container = document.querySelector(".container")
const containerW = container.offsetWidth
root.style.transform = `rotate(90deg)` // 旋转可能会使window.innerWidth、window.innerHeight变化
root.style.transformOrigin = `${containerW / 4}px ${containerW / 4}px`
}
}, [])
return (
);
}
图表中自带图片保存工具,可定位至左侧中间。
toolbox:{
left:0,
top:'middle',
feature: {
saveAsImage: {name:'李氏家谱'}
}
},
经尝试,这个保存按钮在微信浏览器中不起作用,使用自带浏览器打开时,苹果可以保存而安卓无效(issue:Android phones cannot save images using echarts' toolbox-saveAsImage/安卓手机用echarts自带的保存图片工具无法保存 · Issue #13925 · apache/echarts · GitHub),但电脑上是没问题的。考虑到乡亲们的需求情况,家谱信息更新不会太频繁(目前都是我手动修改json文件),我干脆把完整输出的图片挂个链接到页面上,需要的人自己下载即可。这样的副作用就是每次更新家谱后我需手动同步一下图片。
import outputPng from '../../assets/img/family_li.png'
……
return
当然读者小伙伴们根据需要可以考虑用输出网页为图片的各种方法。