本文作者:@猫神 viser-graph 地址:https://github.com/viserjs/viser/tree/master/packages viser-graph 官网demo:https://viserjs.gitee.io/demoHome.html

G6 地址:https://github.com/antvis/G6

前言

自2014年MVVM框架开始冒出苗头,到现如今react、angular、vue三驾马车并驾齐驱。查看谷歌趋势、百度指数、npm生态三大流量来源,可以看出来angular 已经逐渐落后,react 一骑绝尘国外断层式领先、生态最完善,vue渐渐在国内占据主要份额,但并没有哪一个框架能真正一统江湖。
image.pngimage.png
image.png
由此可见,我们框架使用者在图可视化开发时,我们面临几个问题:

  1. 如果没有基于框架封装的可视化库,需要采用函数式调用可视化库,放弃了MVVM框架的优势
  2. 如果切换框架,可能面临切换图可视化库,重新熟悉一套api
  3. 类似d3、g2 此类过程式语法开发,势必提升api 理解成本,提高开发门槛。

在这个背景下,viser-graph应运而生,提供一套api 完美解决3大框架开发者图可视化需求。 viser-graph底层基于g6(国内TOP1图可视化引擎),抽象出一套组件式图可视化开发模式。
image.png
viser-graph 的能力是完全与g6 对标,只要g6 能实现的viser-graph 通通满足你,还会增加一些小点心,提高开发效率哦。例如内置常用的utils 方法:

  • strLen 计算字符长度
  • ellipsisString 字符串过长以…呈现

当然这些常用方法,也期望大家的共建,大家的输入才能让viser-graph 更好用。

解密

下面来说说viser-graph 怎么从json转化成一张图的吧,以下皆以react 框架的实现为例,我们首先需要明确每一层是做什么事情:

  • viser-graph-react:将组件解析成完整的json
  • viser-graph: 将json 对应成 g6 的api,渲染成图

    viser-graph-react

    首先我们拆分基础组件

  • graph:画布最外层组件,包括数据的传入、容器的定义、布局的定义等。

  • node:节点的定义,包括节点的format(样式、大小) 等,目前组件的属性都封装的比较简单,满足基本的诉求。
  • edge:边的定义,包括边的的format(样式、大小) 等,满足基本的诉求。
  • zoom:画布缩放,包括当前缩放比率、最大缩放比率、最小缩放比例等。

以上API 都可以查看 定义文件 哦,或者查阅 文档 ~

详细可见 案例, 定义了一颗紧凑树,包括graph、node、edge的定义。
image.png

  1. import * as React from 'react';
  2. import { Graph, Node, Edge } from 'viser-graph-react';
  3. const data = {
  4. id: "Modeling Methods",
  5. children: [
  6. {
  7. id: "Classification",
  8. children: [
  9. { id: "Logistic regression" },
  10. { id: "Linear discriminant analysis" },
  11. { id: "Rules" },
  12. { id: "Decision trees" },
  13. { id: "Naive Bayes" },
  14. { id: "K nearest neighbor" },
  15. { id: "Probabilistic neural network" },
  16. { id: "Support vector machine" }
  17. ]
  18. },
  19. {
  20. id: "Consensus",
  21. children: [
  22. {
  23. id: "Models diversity",
  24. children: [
  25. { id: "Different initializations" },
  26. { id: "Different parameter choices" },
  27. { id: "Different architectures" },
  28. { id: "Different modeling methods" },
  29. { id: "Different training sets" },
  30. { id: "Different feature sets" }
  31. ]
  32. },
  33. {
  34. id: "Methods",
  35. children: [
  36. { id: "Classifier selection" },
  37. { id: "Classifier fusion" }
  38. ]
  39. },
  40. {
  41. id: "Common",
  42. children: [
  43. { id: "Bagging" },
  44. { id: "Boosting" },
  45. { id: "AdaBoost" }
  46. ]
  47. }
  48. ]
  49. },
  50. {
  51. id: "Regression",
  52. children: [
  53. { id: "Multiple linear regression" },
  54. { id: "Partial least squares" },
  55. { id: "Multi-layer feedforward neural network" },
  56. { id: "General regression neural network" },
  57. { id: "Support vector regression" }
  58. ]
  59. }
  60. ]
  61. };
  62. const graph = {
  63. data,
  64. container: 'mount',
  65. type: 'tree',
  66. width: 500,
  67. height: 500,
  68. pixelRatio: 2,
  69. renderer: 'svg',
  70. fitView: true,
  71. modes: {
  72. default: ['collapse-expand', 'drag-canvas']
  73. },
  74. defaultNode: {
  75. size: 26,
  76. anchorPoints: [[ 0, 0.5 ], [ 1, 0.5 ]],
  77. },
  78. layout: {
  79. type: 'compactBox',
  80. direction: 'LR',
  81. defalutPosition: [],
  82. getId(d) { return d.id; },
  83. getHeight() { return 16 },
  84. getWidth() { return 16 },
  85. getVGap() { return 10 },
  86. getHGap() { return 100 }
  87. }
  88. };
  89. const node = {
  90. formatter: node => {
  91. return {
  92. size: 26,
  93. style: {
  94. fill: '#C6E5FF',
  95. stroke: '#5B8FF9'
  96. },
  97. label: node.id,
  98. labelCfg: {
  99. offset: 10,
  100. position: node.children && node.children.length > 0 ? 'left' : 'right'
  101. }
  102. }
  103. }
  104. }
  105. const edge = {
  106. formatter: () => {
  107. return {
  108. shape: 'cubic-horizontal',
  109. color: '#A3B1BF',
  110. }
  111. },
  112. }
  113. export default class App extends React.Component {
  114. constructor(props) {
  115. super(props);
  116. }
  117. render() {
  118. return (
  119. <div>
  120. <Graph {...graph}>
  121. <Node {...node}/>
  122. <Edge {...edge}/>
  123. </Graph>
  124. </div>
  125. );
  126. }
  127. }

viser-graph-react 递归解析组件,解析出子组件的json,然后通过 context 传递子组件json给最外层的graph,组装成完整的图json( 如下 ),然后再调用 viser-graph 进行渲染。

  1. {
  2. data,
  3. graph: {
  4. container: 'mount',
  5. type: 'tree',
  6. width: 500,
  7. height: 500,
  8. pixelRatio: 2,
  9. renderer: 'svg',
  10. modes: {
  11. default: ['collapse-expand', 'drag-canvas']
  12. },
  13. fitView: true,
  14. layout: {
  15. type: 'compactBox',
  16. direction: 'LR',
  17. defalutPosition: [],
  18. getId(d) { return d.id; },
  19. getHeight() { return 16 },
  20. getWidth() { return 16 },
  21. getVGap() { return 10 },
  22. getHGap() { return 100 }
  23. }
  24. },
  25. node: {
  26. formatter: node => {
  27. return {
  28. size: 26,
  29. anchorPoints: [[0,0.5], [1,0.5]],
  30. style: {
  31. fill: '#C6E5FF',
  32. stroke: '#5B8FF9'
  33. },
  34. label: node.id,
  35. labelCfg: {
  36. offset: 10,
  37. position: node.children && node.children.length > 0 ? 'left' : 'right'
  38. }
  39. }
  40. }
  41. },
  42. edge: {
  43. formatter: () => {
  44. return {
  45. shape: 'cubic-horizontal',
  46. color: '#A3B1BF',
  47. }
  48. },
  49. },
  50. }

viser-graph

viser-graph的设计是将各子组件的json与g6的api 对应,调用g6 的api 进行绘制。可见如下:

  1. // 文件:viser-graph/src/graph.ts
  2. public render() {
  3. // 初始化G6 实例
  4. this.setGraph();
  5. // 设置节点属性
  6. this.setNode();
  7. // 设置边属性
  8. this.setEdge();
  9. // 设置数据
  10. this.setData();
  11. // 设置缩放
  12. this.setZoom();
  13. // TODO: 后续可进一步拆分 tooltip layout plugin 等,避免graph 属性过于堆积
  14. // 调用实例render方法进行渲染
  15. this.graph.render();
  16. // 渲染完成后进行事件绑定
  17. this.setEvent();
  18. }

viser-graph 这一层做的非常轻薄, 只是根据json 调用g6的api ,进行绘制。

其他 vue 、ng 框架也是类似,viser-graph-vue 、viser-graph-ng负责将组件解析成json,调用 viser-graph 进行最后的绘制。

无缝对接G6

除了我们封装的api 外,可能还有些场景需要直接操作g6 的api,那么我们怎么办?很简单,viser-graph-react 暴露了 G6 的入口,使用如下所示:

  1. import * as React from 'react';
  2. // GlobalG6 入口
  3. import { Graph, Node, Edge, GlobalG6 } from 'viser-graph-react';
  4. // 直接调用 GlobalG6 进行注册节点
  5. GlobalG6.registerNode('file-node', {
  6. draw: function draw(cfg, group) {
  7. const keyShape = group.addShape('rect', {
  8. attrs: {
  9. x: cfg.x - 4,
  10. y: cfg.y - 12,
  11. fill: '#fff',
  12. stroke: null
  13. }
  14. });
  15. if (cfg.collapsed) {
  16. group.addShape('marker', {
  17. attrs: {
  18. symbol: 'triangle',
  19. x: cfg.x + 4,
  20. y: cfg.y - 2,
  21. r: 4,
  22. fill: '#666'
  23. }
  24. });
  25. } else if (cfg.children && cfg.children.length > 0) {
  26. group.addShape('marker', {
  27. attrs: {
  28. symbol: 'triangle-down',
  29. x: cfg.x + 4,
  30. y: cfg.y - 2,
  31. r: 4,
  32. fill: '#666'
  33. }
  34. });
  35. }
  36. const shape = group.addShape('text', {
  37. attrs: {
  38. x: cfg.x + 15,
  39. y: cfg.y + 4,
  40. text: cfg.name,
  41. fill: '#666',
  42. fontSize: 16,
  43. textAlign: 'left'
  44. }
  45. });
  46. const bbox = shape.getBBox();
  47. keyShape.attr({
  48. width: bbox.width + 20,
  49. height: bbox.height + 4
  50. });
  51. return keyShape;
  52. }
  53. });
  54. // 直接调用 GlobalG6 进行注册边
  55. GlobalG6.registerEdge('step-line', {
  56. getControlPoints: function getControlPoints(cfg) {
  57. const startPoint = cfg.startPoint;
  58. const endPoint = cfg.endPoint;
  59. return [{
  60. x: startPoint.x,
  61. y: endPoint.y
  62. }];
  63. }
  64. }, 'polyline');
  65. const data = {
  66. id: '1',
  67. name: 'src',
  68. children: [{
  69. id: '1-1',
  70. name: 'behavior',
  71. children: []
  72. }, {
  73. id: '1-3',
  74. name: 'graph',
  75. children: [{
  76. id: '1-3-1',
  77. name: 'controller',
  78. children: []
  79. }]
  80. }, {
  81. id: '1-5',
  82. name: 'item',
  83. children: []
  84. }, {
  85. id: '1-6',
  86. name: 'shape',
  87. children: [{
  88. id: '1-6-2',
  89. name: 'extend',
  90. children: []
  91. }]
  92. }, {
  93. id: '1-7',
  94. name: 'util',
  95. children: []
  96. }]
  97. };
  98. const graph = {
  99. data,
  100. container: 'mount',
  101. type: 'tree',
  102. width: 500,
  103. height: 500,
  104. pixelRatio: 2,
  105. renderer: 'svg',
  106. fitView: true,
  107. modes: {
  108. default: ['collapse-expand', 'drag-canvas']
  109. },
  110. defaultNode: {
  111. shape: 'file-node',
  112. },
  113. defaultEdge: {
  114. style: {
  115. stroke: '#A3B1BF'
  116. }
  117. },
  118. layout: {
  119. type: 'indented',
  120. isHorizontal: true,
  121. direction: 'LR',
  122. indent: 30,
  123. getHeight() { return 16 },
  124. getWidth() { return 16 },
  125. }
  126. };
  127. const node = {
  128. formatter: node => {
  129. return {
  130. shape: 'file-node',
  131. label: node.name
  132. }
  133. }
  134. }
  135. const edge = {
  136. formatter: () => {
  137. return {
  138. shape: 'step-line',
  139. style: {
  140. stroke: '#A3B1BF'
  141. }
  142. }
  143. },
  144. }
  145. export default class App extends React.Component {
  146. constructor(props) {
  147. super(props);
  148. }
  149. render() {
  150. return (
  151. <div>
  152. <Graph {...graph}>
  153. <Node {...node}/>
  154. <Edge {...edge}/>
  155. </Graph>
  156. </div>
  157. );
  158. }
  159. }

通过透传G6 入口,完整提供g6 的能力,不再受限于viser-graph-react封装的局限性,给开发带来更多的可能性。

结语

热烈欢迎大家来使用viser-graph,更热烈欢迎一起来开发的同学,如果发现配置有缺失,可以直接提交PR哈,也是非常简单的。期望大家能使用viser-graph迈入图可视化开发的大门,正向输入业务上的通用场景,帮助viser-graph更好的服务大家。
链接