公司产品因为业务发展,出现了一个新的需求:需要去实现知识库的层级知识展示,展示效果通过树图来实现,具体的展示形式可见下图:

其中有几个需要注意点:

  1. 节点上的详情icon可以点击,点击展开关闭详情
  2. 节点后的伸缩icon在伸缩状态下需要显示当前节点的子节点个数

这个效果有点类似xmind的交互效果了,但是树的节点不论是样式还是点击事件都被高度定制了,在这种情况下基于配置的Echarts们就无用武之地了,我们只能利用更加底层的G6图表引擎去实现。

具体如何安装G6可以参见G6的文档,下面仅仅是选用文档中的第二种安装方式快速引入,写个demo出来验证可行。

首先我们需要完成G6的初始化等前置准备工作

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>树图</title>
<style>
::-webkit-scrollbar {
display: none;
} html, body {
background-color: #f0f2f5;
overflow: hidden;
margin: 0;
}
</style>
</head>
<body>
<div id="mountNode"></div>
<script src="https://gw.alipayobjects.com/os/antv/pkg/_antv.g6-3.1.0/build/g6.js"></script>
<script src="https://gw.alipayobjects.com/os/antv/pkg/_antv.hierarchy-0.5.0/build/hierarchy.js"></script>
<script src="https://gw.alipayobjects.com/os/antv/assets/lib/jquery-3.2.1.min.js"></script>
<script>
const CANVAS_WIDTH = window.innerWidth;
const CANVAS_HEIGHT = window.innerHeight; // 使用G6的TreeGraph
graph = new G6.TreeGraph({
container: "mountNode", width: CANVAS_WIDTH,
height: CANVAS_HEIGHT,
defaultNode: {
shape: "rect",
},
defaultEdge: {
shape: "cubic-horizontal",
style: {
stroke: "rgba(0,0,0,0.25)"
}
},
layout: (data) => {
return Hierarchy.compactBox(data, {
direction: "LR",
getId: function getId(d) {
return d.id;
},
getWidth: function getWidth() {
return 243;
},
getVGap: function getVGap() {
return 24;
},
getHGap: function getHGap() {
return 50;
}
});
}
}); function formatData(data) {
const recursiveTraverse = function recursiveTraverse(node, level) {
const targetNode = {
id: node.itemId + '',
level: level,
type: node.value,
name: node.name,
value: node.content,
collapsed: level > 0,
showDetail: false,
origin: node,
};
if (node.children) {
targetNode.children = [];
node.children.forEach(function (item) {
targetNode.children.push(recursiveTraverse(item, level + 1));
});
}
return targetNode;
};
return recursiveTraverse(data, 0);
} // 获取数据,渲染图表
$.getJSON('https://eliteapp.fanruan.com/certification/data.json', function (data) {
data = formatData(data);
graph.data(data);
graph.render();
});
</script>
</body>
</html>

之后我们便开始我们的自定义节点的设置,可以参考下自定义节点的文档

const getNodeConfig = function getNodeConfig(node) {
let config = {
basicColor: "#722ED1",
fontColor: "rgb(51, 51, 51)",
bgColor: "#ffffff"
};
// 请无视这种中文的判断,这里获取的数据为中文,就不做额外处理,直接拿来判断了
switch (node.type) {
case "标签": {
config = {
basicColor: 'rgba(61, 77, 102, 1)',
fontColor: "rgb(51, 51, 51)",
bgColor: "#ffffff"
};
break;
}
case "分类": {
config = {
basicColor: 'rgba(159, 230, 184, 1)',
fontColor: "rgb(51, 51, 51)",
bgColor: "#ffffff"
};
break;
}
case "业务问题":
config = {
basicColor: "rgba(45, 183, 245, 1)",
fontColor: "rgb(51, 51, 51)",
bgColor: "#ffffff"
};
break;
default:
break;
}
return config;
}; const nodeBasicMethod = {
createNodeBox: function createNodeBox(group, config, width, height, isRoot) {
// 最外面的大矩形,作为节点元素的容器
const container = group.addShape("rect", {
attrs: {
x: 0,
y: 0,
width: width,
height: height,
},
className: 'node-container',
});
if (!isRoot) {
// 不是跟节点,创建左边的小圆点
group.addShape("circle", {
attrs: {
x: 3,
y: height / 2,
r: 6,
fill: config.basicColor
},
className: 'node-left-circle',
});
}
// 节点标题的矩形
group.addShape("rect", {
attrs: {
x: 3,
y: 0,
width: width - 19,
height: height,
fill: config.bgColor,
radius: 2,
cursor: "pointer"
},
className: 'node-main-container',
}); // 节点标题左边的粗线
group.addShape("rect", {
attrs: {
x: 3,
y: 0,
width: 3,
height: height,
fill: config.basicColor,
},
className: 'node-left-line',
});
return container;
},
createDetailIcon: function createDetailIcon(group) {
// icon外面的矩形,用来计算icon的宽度
const iconRect = group.addShape("rect", {
attrs: {
fill: "#FFF",
radius: 2,
cursor: "pointer"
}
});
iconRect.attr({
x: 154,
y: 6,
width: 24,
height: 24
});
// 设置icon的图片
group.addShape("image", {
attrs: {
x: 154,
y: 6,
height: 24,
width: 24,
img: "https://eliteapp.fanruan.com/web-static/media/close.svg",
cursor: "pointer",
opacity: 1
},
className: "node-detail-icon"
});
// 放一个透明的矩形在 icon 区域上,方便监听点击
group.addShape("rect", {
attrs: {
x: 160,
y: 12,
width: 12,
height: 12,
fill: "#FFF",
cursor: "pointer",
opacity: 0
},
className: "node-detail-box",
});
return iconRect.getBBox().width;
},
createNodeName: (group, config) => {
group.addShape("text", {
attrs: {
// 根据 icon 的宽度计算出剩下的留给 name 的长度
text: "node title",
x: 18,
y: 18,
fontSize: 13,
fontWeight: 400,
textAlign: "left",
textBaseline: "middle",
fill: config.fontColor,
cursor: "pointer"
},
className: 'node-name-text',
});
},
createNodeDetail: function createNodeDetail(group, config) {
// 节点的类别说明,即 # 业务问题
group.addShape('text', {
attrs: {
text: '',
x: 18,
y: 45,
fontSize: 10,
lineHeight: 16,
textAlign: "left",
textBaseline: "middle",
fill: config.basicColor,
cursor: "pointer",
},
className: 'node-detail-info'
});
// 节点的详情
group.addShape("text", {
attrs: {
text: '',
x: 18,
y: 45,
fontSize: 11,
lineHeight: 16,
textAlign: "left",
textBaseline: "middle",
fill: 'rgb(51, 51, 51)',
cursor: "pointer",
},
className: "node-detail-text",
});
// 节点的 查看详情 按钮
group.addShape('text', {
attrs: {
text: '',
x: 18,
y: 61,
fontSize: 11,
lineHeight: 16,
textAlign: "left",
textBaseline: "middle",
fill: config.basicColor,
cursor: "pointer",
},
className: "node-detail-link",
});
// 节点的 反馈问题 按钮
group.addShape('text', {
attrs: {
text: '',
x: 99,
y: 61,
fontSize: 11,
lineHeight: 16,
textAlign: "left",
textBaseline: "middle",
fill: config.basicColor,
cursor: "pointer",
},
className: "node-detail-feedback",
});
},
createNodeMarker: function createNodeMarker(group, collapsed, x, y, childrenNum) {
// 伸缩按钮的圆形背景
group.addShape("circle", {
attrs: {
x: x,
y: y,
r: 13,
fill: "rgba(47, 84, 235, 0.05)",
opacity: 0,
zIndex: -2
},
className: "collapse-icon-bg"
});
// 伸缩按钮的 节点数量 文字
group.addShape("text", {
attrs: {
x: x,
y: y + (7 / 2),
text: collapsed ? childrenNum : '-',
textAlign: "center",
fontSize: 10,
lineHeight: 7,
stroke: "rgba(0,0,0,0.25)",
fill: "rgba(0,0,0,0)",
opacity: 1,
cursor: "pointer"
},
className: "collapse-icon-num"
});
// 伸缩按钮的圆形边框
group.addShape("circle", {
attrs: {
x: x,
y: y,
r: 7,
stroke: "rgba(0,0,0,0.25)",
fill: "rgba(0,0,0,0)",
opacity: 1,
cursor: "pointer"
},
className: "collapse-icon"
});
},
}; const TREE_NODE = "tree-node";
G6.registerNode(TREE_NODE, {
drawShape: function drawShape(cfg, group) {
// 获取节点的颜色配置
const config = getNodeConfig(cfg);
const isRoot = cfg.type === "标签";
// 最外面的大矩形
// 这里的宽度为写死的宽度,全部节点的宽度统一,高度为data在处理时赋予的高度
const container = nodeBasicMethod.createNodeBox(group, config, NODE_WIDTH, cfg.nodeHeight, isRoot);
// 创建节点详情展开关闭的icon
nodeBasicMethod.createDetailIcon(group);
// 创建节点标题
nodeBasicMethod.createNodeName(group, config);
// 创建节点详情
nodeBasicMethod.createNodeDetail(group, config); const childrenNum = (cfg.children || []).length;
if (childrenNum > 0) {
// 创建节点的伸缩icon
nodeBasicMethod.createNodeMarker(group, cfg.collapsed, 191, 18, childrenNum);
} return container;
},
}, "single-shape"); defaultNode: {
// 在G6的初始化中将节点改为使用自定义的节点
shape: TREE_NODE,
},

此时,我们便可得到如图的示例:

但是这里的跟节点的连线位置是四分五裂的,我们的交互图是统一到右侧中间伸缩icon右侧和节点左侧中间的,所以接下来我们需要对节点连线的控制点进行适配,节点的连接控制点可以参见G6的文档

defaultNode: {
shape: TREE_NODE,
// 全局设置节点的锚点控制点,分别在左侧中间和右侧中间
anchorPoints: [[0, 0.5], [1, 0.5]]
},

此时的树形图便如下:

到这里之后,节点的效果图已经出来了,但是节点的详情交互还未实现,接下来开始实现详情的交互。

节点的交互主要为展开关闭节点详情、展开伸缩子树。

展开关闭节点详情由用户点击下拉icon触发,所以我们就需要监听节点的点击事件再具体一点就是监听节点icon的点击事件。

// 由于节点的文本不会换行,根据节点的宽度切分节点详情文本到数组中,然后进行换行
const fittingStringLine = function fittingStringLine(str, maxWidth, fontSize) {
str = str.replace(/\n/gi, '');
const fontWidth = fontSize * 1.3; //字号+边距 const actualLen = Math.floor(maxWidth / fontWidth);
let width = strLen(str) * fontWidth;
let lineStr = [];
while (width > 0) {
const substr = str.substring(0, actualLen);
lineStr.push(substr); str = str.substring(actualLen);
width = strLen(str) * fontWidth;
}
return lineStr;
}; const strLen = function strLen(str) {
let len = 0;
if(!str) {
return len;
} for (let i = 0; i < str.length; i++) {
if (str.charCodeAt(i) > 0 && str.charCodeAt(i) < 128) {
len++;
} else {
len += 2;
}
}
return len;
}; const nodeBasicMethod = { afterDraw: function afterDraw(cfg, group) {
// 伸缩icon的背景色交互
const collapseIcon = group.findByClassName("collapse-icon");
if (collapseIcon) {
const bg = group.findByClassName("collapse-icon-bg");
// 监听事件
collapseIcon.on("mouseenter", function () {
bg.attr("opacity", 1);
graph.get("canvas").draw();
});
collapseIcon.on("mouseleave", function () {
bg.attr("opacity", 0);
graph.get("canvas").draw();
});
} // 下拉展示与隐藏节点详情
const nodeDetailBox = group.findByClassName("node-detail-box");
nodeDetailBox.on("click", function () {
nodeBasicMethod.handleDetail(cfg, group);
});
},
handleDetail: function handleDetail(cfg, group) {
const circle = group.findByClassName('node-left-circle');
const mainContainer = group.findByClassName('node-main-container');
const nodeLeftLine = group.findByClassName('node-left-line');
const rightCircleBg = group.findByClassName('collapse-icon-bg');
const rightCircleIconNum = group.findByClassName('collapse-icon-num');
const rightCircleIcon = group.findByClassName('collapse-icon'); const nodeDetailText = group.findByClassName('node-detail-text');
const nodeDetailInfo = group.findByClassName('node-detail-info');
const nodeDetailLink = group.findByClassName('node-detail-link');
const nodeDetailFeedback = group.findByClassName('node-detail-feedback'); // 查找节点在树上的下方节点
const node = graph.findById(cfg.id);
const nodes = graph.findAll('node', item => {
const model = item.getModel();
return model.level === node.getModel().level;
});
const leftNodes = nodes.slice(nodes.indexOf(node) + 1); let nodeHeight;
if (cfg.showDetail) {
// 详情已经展开,开始关闭详情
nodeHeight = NODE_HEIGHT; // 关闭详情
nodeDetailText.attr('text', '');
nodeDetailInfo.attr('text', '');
nodeDetailLink.attr('text', '');
nodeDetailFeedback.attr('text', ''); // 下方节点上移
leftNodes.forEach((leftNode) => {
leftNode.getModel().y = leftNode.getBBox().y - 80;
graph.updateItem(leftNode, {
y: leftNode.getBBox().y - cfg.nodeHeight + NODE_HEIGHT,
});
}); cfg.showDetail = false;
} else {
// 详情未展开,开始展开详情 // 展示详情
const detailText = fittingStringLine(cfg.value, 198, 12);
nodeDetailText.attr('text', detailText.join('\n'));
nodeDetailText.attr('y', 45 + 16 + (detailText.length) * 8); nodeDetailInfo.attr('text', `# ${cfg.type}`);
nodeDetailLink.attr('text', '查看详情');
nodeDetailLink.attr('y', 45 + 16 + (detailText.length) * 16 + 16);
nodeDetailFeedback.attr('text', '反馈问题');
nodeDetailFeedback.attr('y', 45 + 16 + (detailText.length) * 16 + 16); nodeHeight = 45 + 16 + (detailText.length + 1) * 16 + 16; // 下方的节点下移
leftNodes.forEach((leftNode) => {
leftNode.getModel().y = leftNode.getBBox().y + 80;
graph.updateItem(leftNode, {
y: leftNode.getBBox().y + nodeHeight - cfg.nodeHeight,
});
}); cfg.showDetail = true;
}
cfg.nodeHeight = nodeHeight; // 调节节点元素高度
circle.attr('y', nodeHeight / 2);
mainContainer.attr('height', nodeHeight);
nodeLeftLine.attr('height', nodeHeight);
if (rightCircleBg && rightCircleIconNum && rightCircleIcon) {
rightCircleBg.attr('y', nodeHeight / 2);
// 计算伸缩icon的位置,G6在这里有个坑,canvas模式下的文本位置会产生偏差
rightCircleIconNum.attr('y', nodeHeight / 2 + 5 + (nodeHeight - NODE_HEIGHT) * 0.1);
rightCircleIcon.attr('y', nodeHeight / 2);
} // 更新当前节点的高度
graph.updateItem(node, Object.assign(cfg, {
style: {
height: nodeHeight,
},
}));
graph.get('canvas').draw();
},
};
G6.registerNode(TREE_NODE, {
drawShape: function drawShape(cfg, group) {},
// 设置监听
afterDraw: nodeBasicMethod.afterDraw,
}, "single-shape");

此时,展开关闭详情的交互就已经实现了,如图:

对于伸缩的交互,G6提供的树图自带了专用的伸缩Behavior,可以直接拿过来进行定制使用。

graph = new G6.TreeGraph({
container: "mountNode", width: CANVAS_WIDTH,
height: CANVAS_HEIGHT,
defaultNode: {},
defaultEdge: {},
modes: {
default: [{
type: "collapse-expand",
// 判断是否开始伸缩
shouldBegin: function shouldBegin(e) {
console.log('shouldBegin', e.target.get("className") === "collapse-icon");
// 点击 node 禁止展开收缩,只有在点击到的是伸缩icon的时候才允许伸缩
return e.target.get("className") === "collapse-icon";
},
// 伸缩状态发生改变
onChange: function onChange(item, collapsed) {
const icon = item.get("group").findByClassName("collapse-icon-num");
icon.attr("text", collapsed ? item.getModel().children.length : '-'); // 关闭全部的详情
const detailNodeList = graph.findAll('node', node => {
return node.getModel().showDetail;
});
detailNodeList.forEach(detailNode => {
const group = detailNode.get('group');
const cfg = detailNode.getModel(); nodeBasicMethod.handleDetail(cfg, group);
});
},
}]
},
layout: (data) => {}
});

我们的树图便可以正常的伸缩啦,如图:

完整代码可暂时从这儿下载:https://files.cnblogs.com/files/tingyugetc/g6-tree.zip

基于G6画个xmind出来的更多相关文章

  1. 基于Go语言的xmind读写库,我主要用来把有道云笔记思维导图转为xmind

    项目地址 xmind 基于go语言的xmind接口 使用方法参考: example 本库主要加载xmind文件为json结构,保存文件时也用的json结构而不是xml结构 本库只做了最基本的主题添加功 ...

  2. Asp.Net Core 使用Quartz基于界面画接口管理做定时任务

    今天抽出一点点时间来造一个小轮子,是关于定时任务这块的. 这篇文章主要从一下几点介绍: 创建数据库管理表 创建web项目 引入quarzt nuget 包 写具体配置操作,实现定时任务处理 第一步:创 ...

  3. C#基于两种需求向图片添加水印

    使用场景 1.也就是大家经常用的,一般是图片的4个角落,基于横纵坐标来添加. 2.在图片内基于固定位置,文字始终居中.刚开始我基于第一种场景来根据水印汉字的长度来计算坐标,后来发现方法始终不可靠.现在 ...

  4. G6 知识点

    Viser 一个基于 G2 实现的,为数据可视化工程师量身定制的工具. Viser-Graph 一个基于 G6 实现的,为呈现关系型数据的定制化工具. Mode 是 G6 提供的图上事件的管理机制. ...

  5. G6 学习资料

    G6 学习资料 网址 G6 1.x API 文档 http://antvis.github.io/g6/doc/index.html 官方demo列表 https://github.com/antvi ...

  6. G6:AntV 的图可视化与图分析

    导读 G6 是 AntV 旗下的一款专业级图可视化引擎,它在高定制能力的基础上,提供简单.易用的接口以及一系列设计优雅的图可视化解决方案,是阿里经济体图可视化与图分析的基础设施.今年 AntV 11. ...

  7. Swift 全功能的绘图板开发

    要做一个全功能的绘图板,至少要支持以下这些功能: 支持铅笔绘图(画点) 支持画直线 支持一些简单的图形(矩形.圆形等) 做一个真正的橡皮擦 能设置画笔的粗细 能设置画笔的颜色 能设置背景色或者背景图 ...

  8. C# WPF动点任意移动气泡画法(解决方案使用到数学勾股定理、正弦定理、向量知识)。

    许久没写博客了,最近在研究WPF下气泡的画法,研发过程还是比较艰辛的(主要是复习了高中的数学知识,MMP全忘光了),这篇博客主要是提供一个思路给大家参考,如果有大神还有更好的解决方案可以不吝您的言论尽 ...

  9. ELASTIC制图等高级使用

    基于上一个安装部署的文档后(ELASTIC 5.2部署并收集nginx日志) http://www.cnblogs.com/kerwinC/p/6387073.html 本次带来一些使用的分享. ki ...

随机推荐

  1. 刷题75. Sort Colors

    一.题目说明 题目75. Sort Colors,给定n个整数的列表(0代表red,1代表white,2代表blue),排序实现相同颜色在一起.难度是Medium. 二.我的解答 这个是一个排序,还是 ...

  2. rancher布控集群启动失败

    rancher布控集群启动失败 待办 报告缺少某个文件.多线程启动任务部署的时候某些线程跑在前边了, 导致问题出现 解决思路:等待,等待响应的job重启就ok了,都是一些job在跑,失败了会重新开始的 ...

  3. php设计模式之多态实例代码

    <?php header("Content-type:text/html;charset=utf-8"); /** * 虎 */ abstract class Tiger { ...

  4. 959F - Mahmoud and Ehab and yet another xor task xor+dp(递推形)+离线

    959F - Mahmoud and Ehab and yet another xor task xor+dp+离线 题意 给出 n个值和q个询问,询问l,x,表示前l个数字子序列的异或和为x的子序列 ...

  5. python记之Hello world!

    ________________________________该动手实践了. 数和表达式 交互式Python解释器可用作功能强大的计算器. 除法运算的结果为小数,即浮点数(float或floatin ...

  6. vue-cli 3 脚手架搭建(create)

    地址:https://cli.vuejs.org/zh/guide/ 安装步骤: 提示:node 版本要 8.9+ 两种方式: (1) npm install -g @vue/cli (2) yarn ...

  7. springboot中druid监控的配置(DruidConfiguration)

    当数据库连接池使用druid 时,我们进行一些简单的配置就能查看到sql监控,web监控,url监控等等. 以springboot为例,配置如下 import com.alibaba.druid.su ...

  8. jquery grid 获取选中的行的数据,以及获取所有行的方法

    https://blog.csdn.net/shenqingkeji/article/details/52861319

  9. 牛客1080D tokitsukaze and Event (双向最短路)

    题目链接:https://ac.nowcoder.com/acm/contest/1080/D 首先建两个图,一个是权值为a的图,一个是权值为b的图. 从s起点以spfa算法跑权值为ai的最短路到t点 ...

  10. 移动端CSS重置

    移动端 CSS Reset 该怎么写 为了应对各大浏览器厂商对浏览器默认样式的不统一处理,我们往往会进行一个 css reset 操作,由于没有标准而且受个人偏好影响,每个公司实现的都不尽相同.关于 ...