首发于cloudjfed
GoJS 初探

GoJS 初探

GoJS 是什么

GoJS是一个功能丰富的js库,用于在浏览器上实现自定义交互式图表和复杂的可视化图表。 GoJS通过可自定义的模板和布局构建复杂节点,链接和组,绘制js图表。

GoJS为用户交互提供了许多高级功能,如拖放,复制和粘贴,文本编辑,工具提示,上下文菜单,自动布局,数据绑定和模型,事务状态和撤销管理,事件处理程序,命令以及用于自定义操作的可扩展工具系统等等。


为什么使用GoJS

为了更直观地表达信息,我们常常需要用图形来展示数据以及逻辑关系,例如最常见的架构图、ER图、流程图、BPMN图等等,都是用来解决实际应用所遇到的问题。前端可使用canvas,svg,html+css技术来绘制图形。主流的前端库有mxGraph,Joint等这篇文章介绍的比较清楚了。

我们对用户交互有很高的需求,并且有深入定制模板的需求,因此我们选择GoJS


如何使用GoJS

官网上介绍:GoJS支持图形模板 以及 图形对象属性与模型数据的数据绑定。

GoJS supports graphical templates and data-binding of graphical object properties to model data.

我们通过下面两个部分,来理解这句话。


GoJS 的概念

图表(Diagram)

GoJS图表即最后看到的可视化视图,它是由这些部分构成的:一个或多个可能有连接关系的、可能成组的节点。所有这些节点和链路聚集在相同或不同的层中,并呈现出一定的布局(开发者预定好的或GoJS自动布局)。


模型(Modal)

每个图表都有一个数据模型,用于保存和解释开发者程序的数据。

模型描述了节点之间的连接关系和组成员关系。图表自动为模型 Model.nodeDataArray 中的每个数据项创建一个节点或组, 为模型 GraphLinksModel.linkDataArray 中的每个数据项创建一个链接。而且,我们可以为每个数据对象添加所需的任何属性。


模板(Template)

模板声明了每个节点或链路的外观、位置和行为。


面板(Panel)

每个模板由GoJS中的面板Panel构成,面板本身作为一个图形对象GraphObject,保存其他图形对象作为它的元素,同时,面板需要负责图形对象的尺寸、位置。

每个面板建立自己的坐标系,面板中的元素按顺序绘制,从而确定了这些元素的z坐标。

面板有很多种类,比如 Panel.Position,Panel.Auto,Panel.Vertical,Panel.Horizontal 等等。

所有部件(这里指构成面板的图形对象,比如 Shapes、Pictures、TextBlocls)都有默认模板。


图形对象属性与模型数据属性的数据绑定使得数据的每个节点或链接都是唯一的。

下面这张图会更直观地解释上面的概念:


一个简单的栗子: Hello world

1. 引入文件

官网下载并引入GoJS,开发期间可加装go-debug.js文件,帮助开发

<script src="go-debug.js"></script>

2. 创建GoJS的容器

创建一个GoJS容器,并设置固定大小,到时候GoJS会添加一个canvas到该容器,并且canvas元素自动调整为容器元素的大小。Tips:高度不能设置为百分比哦

<div id="myDiagramDiv" style="border: solid 1px blue; width:400px; height:150px"></div>

3. 创建图表

现在我们创建图表,并引用该Div元素。请注意,js代码中的所有GoJS类型(如Diagram)的引用都以“ go.” 作为前缀。

下面代码中我们使用了链路模板,添加了节点和链路数据模型。

这里我们没有绘制节点或者链路模板,图表会显示默认样式。

<script>
     var diagram = new go.Diagram("myDiagramDiv");
     diagram.model = new go.GraphLinksModel(
     [{ key: "Hello" },   // 节点数据
      { key: "World!" }],
     [{ from: "Hello", to: "World!"}]  // 链路数据
   );
 </script>

绘制出来如下:


数据双向绑定

数据绑定是指从源对象中提取值并在目标对象上设置属性。目标对象就是图形对象(GraphObject),源对象是模型中保存的js数据对象。

使用模板和数据绑定简化了必须存储在模型数据中的信息,灵活性非常强。当然并不是所有的数据属性都需要绑定使用。


又一个简单的栗子:属性绑定

1. 为了简化绘图时new对象的写法,我们直接将GoJS的构造函数赋值给一个简单的对象。

var $ = go.GraphObject.make;    

可以将这个方法理解为一个笔刷,绘图时使用。

该函数可多层嵌套,参数类型较多。

第一个参数声明类型,这个类型必须是图形对象的子类。

其他参数类型:

  • 具有属性/值对的纯js对象 - 这些属性值是在正在构造的对象上设置的
  • 一个正在作为一个元素被添加到正在构造的面板(Panel)中的图形对象(GraphObject)
  • 一个GoJS枚举值常量,用作被构造的对象的唯一属性的值,可以接受这样一个值
  • 一个字符串,它设置正在构造的对象的TextBlock.textShape.figurePicture.sourcePanel.type属性
  • 一个RowColumnDefinition,用于描述Panel的行或列
  • ...

2. 绘制两个节点,在数据模型中描述节点颜色,并按不同颜色显示

图形的填充色是fill属性,我们把color属性绑定在fill上。GoJS提供了Binding对象,我们将他与GraphObject相关联。

var diagram = $(go.Diagram, "myDiagramDiv");  
diagram.nodeTemplate =
  $(go.Node, "Auto",
    $(go.Shape,
      { figure: "RoundedRectangle", fill: "white" },  
      new go.Binding("fill", "color")), 
    $(go.TextBlock,
      { margin: 5 },
      new go.Binding("text", "key"))  
   );

var nodeDataArray = [
  { key: "Alpha", color: "lightblue" },  
  { key: "Beta", color: "pink" }
];
var linkDataArray = [
  { from: "Alpha", to: "Beta" }
];
diagram.model = new go.GraphLinksModel(nodeDataArray, linkDataArray);

结果如下:


除此之外,go.Binding的第三个参数,可传入一个函数,参数为数据绑定属性的值,返回值为GraphObject被绑定属性的值,比如:

// 数据存在属性color,则返回'lightblue', 否则返回'pink'
new go.Binding("fill","color",function( v ) { 
    return v ? "lightblue":"pink" ;
} 


数据监听

model无法检测到nodeDataArray数组的修改或任何节点数据对象的修改。如果对节点的数据进行修改,并且需要图表自动刷新,则需要视情况调用相关api。

对于绑定属性的变化,需要调用setDataProperty。像下面这样:

function  flash () { 
  var model = diagram.model;
  //所有模型更改都应该在事务模型中进行 
  model.startTransaction("flash");
  var data = model.nodeDataArray[0];  //获取第一个节点数据 
  model.setDataProperty(data, "highlight", !data.highlight); // 修改属性
  model.commitTransaction("flash");
}


对于结构属性,比如添加删除节点,或者修改节点间关系的,api有 setKeyForNodeData, setCategoryForNodeData,GraphLinksModel.setToKeyForLinkData,orGraphLinksModel.setGroupKeyForNodeData等。

或者,你可以注册一个监听事件来监听model节点的变化。

当然,对于链路对象也是一样的。


双向数据绑定(Two-way data binding)

上述所有绑定只将属性的值从源数据转移到目标对象。但有时我们希望能够将GraphObject中的值传输回模型数据,使得模型数据与ui界面的图标中的数据保持一致。这可以通过使用TwoWay 绑定,它可以完成从源数据到目标对象,以及从目标对象到源数据的值传递。

像这样调用makeTwoWay()进行双向绑定:

new go.Binding("location", "loc").makeTwoWay();
...
// 获取节点最新位置
var data = diagram.model.nodeDataArray[0];  
var node = diagram.findNodeForData(data);  
var p = node.location.copy();  // p则取得了节点的位置


双向绑定的目的是,当用户进行界面交互时,节点或者链路任何属性的更改都能保持在模型数据中。这样,我们就能通过保存模型,轻松地保存图表。


实例:绘制动物关系图

现在我们使用GoJS画一个简单的有层级的动物关系图,如下图:

1. 分析

  • 整体布局:顶部居中,从上至下分层。这里我们使用一个布局对象(Diagram.layout),使用树布局。因为GoJS中的树是从左到右排列的,我们让他旋转90度。
  • 交互:允许放大缩小,整体拖拽,不允许单个节点拖拽, 禁用图表的一些默认交互功能
  • 数据模型:虽然布局是树布局,但数据模型不是,所以我们用链路模型
    • 节点:有文字,节点之间存在包含和被包含的情况(组信息)
    • 链路:描述节点之间的关系
  • 模板:
    • 节点:文字被圆角矩形包围
    • 链路:灰色线条,末端有箭头
    • 组:两种样式(黄色边框,黄色背景;蓝色边框,蓝色背景)


2. 获取构造函数开始绘制

var $ = go.GraphObject.make;


3. 拿到画布,并设定基本的配置(操作\布局等)

// 整体布局和用户鼠标键盘交互的配置
var myDiagram = $(go.Diagram, 'myDiagramDiv', {
   initialContentAlignment: go.Spot.TopCenter,
   allowDrop: false,
   allowMove: false,
   allowSelect: false,
   layout: $(go.TreeLayout, {
     angle: 90
   })
});

4. 写数据模型

我们在nodeDataArray中,设置好节点之间的成组关系,用isGroup 表示是否成组,group属性表示属于哪些组,这两个属性是默认的、并且固定地用于描述组和组成员关系。

除此之外,我们还添加了category属性,这个属性也是固定属性,用法下面解释。

我们在linkDataArray中,设置了节点之间的连接关系。

 const nodeDataArray = [
     { key: 1, text: '动物' },
     { key: 2, text: '脊椎动物', isGroup: true, category: 'OfGroups' },
     { key: 3, text: '无脊椎动物', isGroup: true, category: 'OfGroups' },
     { key: 4, text: '鱼类', isGroup: true, category: 'OfNodes', group: 2 },
     { key: 5, text: '鸟类', isGroup: true, category: 'OfNodes', group: 2 },
     { key: 21, text: '哺乳类', isGroup: true, category: 'OfNodes', group: 2 },
     { key: 6, text: '环节动物', isGroup: true, category: 'OfNodes', group: 3 },
     { key: 7, text: '节肢动物', category: 'OfNodes', isGroup: true, group: 3 },
     { key: 8, text: '鲶鱼', group: 4 },
     { key: 9, text: '沙丁鱼', group: 4 },
     { key: 10, text: '麻雀', group: 5 },
     { key: 22, text: '大象', group: 21 },
     { key: 11, text: '蝴蝶', group: 7 },
     { key: 12, text: '蚯蚓', group: 6 },
     { key: 13, text: '会飞', group: 35 },
     { key: 14, text: '会爬', group: 35 },
     { key: 15, text: '会游泳', group: 35 },
     { key: 25, text: '不会动', group: 35 },
     { key: 16, text: '吃的', isGroup: true, group: 36, category: 'OfNodes' },
     { key: 17, text: '喝的', isGroup: true, category: 'OfNodes', group: 36 },
     { key: 18, text: '水果', group: 16 },
     { key: 19, text: '植物', group: 16 },
     { key: 23, text: '水', group: 17 },
     { key: 24, text: '饮料', group: 17 },
     { key: 35, text: '动作', isGroup: true, category: 'OfGroups'},
     { key: 36, text: '食物', isGroup: true, category: 'OfGroups' }
   ];

   const linkDataArray = [
     { from: 1, to: 2 },
     { from: 1, to: 3 },
     { from: 8, to: 15 },
     { from: 9, to: 15 },
     { from: 10, to: 13 },
     { from: 22, to: 15 },
     { from: 12, to: 14 },
     { from: 11, to: 13 },
     { from: 10, to: 18 },
     { from: 22, to: 18 },
     { from: 8, to: 19 },
     { from: 9, to: 19 },
     { from: 10, to: 19 },
     { from: 11, to: 19 },
     { from: 12, to: 19 },
     { from: 22, to: 19 },
     { from: 25, to: 36 }
   ];
   myDiagram.model = new go.GraphLinksModel(nodeDataArray, linkDataArray);

5. 绘制节点模板

每个节点,我希望他有个圆角矩形框,里面填充文字。

节点模板的面板中,go.Node作为顶级面板, go.Shape 和 go.TextBlock 都是子元素,布局关系应该设置成 Auto,同时我们做一些简单的属性绑定。

 myDiagram.nodeTemplate = $(
     go.Node,
     'Auto',
     $(go.Shape, 'RoundedRectangle', { fill: 'white' }),
     $(go.TextBlock, new go.Binding('text', 'text'))
   );


6. 绘制链路模板

因为线条有点多,为了突出节点,希望线条有点弧度,颜色浅一点,同时有标准的箭头指向

 myDiagram.linkTemplate = $(
     Go dot Link: premium domain name,
     go.Link.Bezier,
     $(go.Shape, {
       strokeWidth: 1,
       fill: '#8f99aa',
       stroke: '#8f99aa'
     }),
     $(go.Shape, { toArrow: 'Standard' })
   );

7. 绘制组模板

因为组的嵌套最多的有三级,比如 脊椎动物 > 鱼类 > 沙丁鱼,所以我们设置两个组模板,这时候就用到groupTemplateMap 。当然,节点和链路也有类似的模板地图,用来设置多个样式风格。

这时候,很容易就理解了 节点中catalog的作用

 myDiagram.groupTemplateMap.add(
     'OfGroups',   // 注册组名
     $( go.Group, 'Auto',  // 设置组属性和排版
       {
         background: 'transparent',
         handlesDragDropForMembers: true, 
         // 使用网格布局
         layout: $(go.GridLayout, {
           wrappingWidth: Infinity,
           alignment: go.GridLayout.Position,
           cellSize: new go.Size(10, 10), // 每个part的最小尺寸
           spacing: new go.Size(4, 4) // 间隔
         })
       },
       // 整个黄色的矩形大框框
       $(go.Shape, 'Rectangle', { fill: null, stroke: '#FFDD33', strokeWidth: 2 }), 
       // 填充在矩形框里的标题部分,这里引入了go.Placeholder 对象,这个对象用于存放成员,并做一些填充
       // 标题和成员,我们竖向排版
       $(go.Panel,'Vertical', 
         // 标题模块,我们添加了一个展开收起的按钮,和标题文字是横向排布的
         $( go.Panel, 'Horizontal', 
           { stretch: go.GraphObject.Horizontal, background: '#FFDD33' },
           // 展开收起按钮
           $('SubGraphExpanderButton', { alignment: go.Spot.Right, margin: 5 }),
           // 标题文字和一些设置
           $(go.TextBlock,
             {
               alignment: go.Spot.Left,
               margin: 5,
               font: 'bold 18px sans-serif',
               opacity: 0.75,
               stroke: '#404040'
             },
             new go.Binding('text', 'text')
           )
         ), 
         $(go.Placeholder, { padding: 5, alignment: go.Spot.TopLeft })
       ) 
     )
   );
   myDiagram.groupTemplateMap.add(
     'OfNodes',   // 注册组名
     // 分析同上
     $( go.Group, 'Auto',
       {
         background: 'transparent',
         ungroupable: true,
         computesBoundsAfterDrag: true,
         handlesDragDropForMembers: true, 
         layout: $(go.GridLayout, {
           wrappingColumn: 1,
           alignment: go.GridLayout.Position,
           cellSize: new go.Size(1, 1),
           spacing: new go.Size(4, 4)
         })
       },
       $( go.Shape, 'Rectangle', { fill: null, stroke: '#33D3E5', strokeWidth: 2 }),
       $( go.Panel,'Vertical', 
         $( go.Panel, 'Horizontal', 
           { stretch: go.GraphObject.Horizontal, background: '#33D3E5' },
           $('SubGraphExpanderButton', { alignment: go.Spot.Right, margin: 5 }),
           $( go.TextBlock,
             {
               alignment: go.Spot.Left,
               editable: true,
               margin: 5,
               font: 'bold 16px sans-serif',
               opacity: 0.75,
               stroke: '#404040'
             },
             new go.Binding('text', 'text')
           )
         ), 
         $(go.Placeholder, { padding: 5, alignment: go.Spot.TopLeft })
       ) 
     )
   );


8. 交互行为

如果还想在节点和图表上定义一些键盘和鼠标行为,可以参考这里:Properties Details 表格描述了交互行为的方法、参数。


更多

如果你还没有接触过GoJS, 并对它有兴趣的话,推荐学习路线为 Learn -> Intro -> Samples -> API


总结

GoJS是纯js,因此用户可以在不需要服务器和无插件的情况下愉快地构建自己的可视化视图。它交互行为丰富,自定义模板灵活,这里已经有非常多的例子,足够解决实际业务中的常见图表需求。

当然了,所有的图形都是由点和线组成,如果开发者的想象力足够丰富,可以创造更多有趣、炫酷的图表。

编辑于 2017-09-29 14:05