mxgraph的艰难入门

前言

公司今年有个需求是希望前端能展现出大数据计算过程的拓扑图,最好还是能够动态展现。部门老大给我们推荐了mxgraph这个绘图js库,希望我们能够熟练掌握。于是我就先着手开始研究起来了。不过我现在对于mxgraph的掌握还非常的渣,这篇文章是记录到现在为止我的学习成果。

关于js绘图

绘图这块是前端的一个细分领域,是属于那种做的人不多,但深入下去很有前景的一个方向,当然也是一个比较难的方向。提到js图库,大家一般都会想到的是echarts、3D.js或者three.js之类的。echarts是使用他们给定的图库,大多是图表,改变自己的数据或者是定制一些细节。3D.js和three.js了解不多,但是貌似是从点线开始画起的。如果用来开发,成本难度就稍微有点大。我们公司是希望能够根据自己的需求定制化图形,最好是有一些基础的图元,和封装好的api操作,所以老大给我们推荐了mxgraph。

关于mxgraph

mxgraph库是一个诞生比较久的项目,提供了基础的图元和绘制方法,封装了绘制过程中的基础操作api。有一些比较著名的在线绘图网站就是基于mxgraph二次开发的,比如draw.ioprocess onmxgraph库在github上有三千多个star,提供了90多个demo给使用者阅读,demo展现了绝大多数mxgraph所能做到的事。最常见的上手方式就是查看所给的示例demo,对照着示例的效果,查询对应的api文档查询用法。下面我便通过介绍几个我学习的demo来说明mxgraph的用法。

1.hello world

helloworld.html顾名思义,就像所有的程序学习最开始时,学习如何输出“hello world”一样,这个示例是所有的demo中最基础的一个。介绍了是如何使用mxgraph,绘制两个矩形,并将其连接在一起。我们来看完整的示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
<html>
<head>
<title>Hello, World! example for mxGraph</title>
<script type="text/javascript">
mxBasePath = '../src';
</script>
<script type="text/javascript" src="../src/js/mxClient.js"></script>
<script type="text/javascript">
function main(container)
{
if (!mxClient.isBrowserSupported()){
mxUtils.error('Browser is not supported!', 200, false);
} else {
var graph = new mxGraph(container);
new mxRubberband(graph);
var parent = graph.getDefaultParent()
graph.getModel().beginUpdate();
try
{
var v1 = graph.insertVertex(parent, null, 'Hello,', 20, 20, 80, 30);
var v2 = graph.insertVertex(parent, null, 'World!', 200, 150, 80, 30);
var e1 = graph.insertEdge(parent, null, '', v1, v2);
} finally {
graph.getModel().endUpdate();
}
}
};
</script>
</head>
<body onload="main(document.getElementById('graphContainer'))">
<div id="graphContainer" style="position:relative;overflow:hidden;width:321px;height:241px;background:url('editors/images/grid.gif');cursor:default;">
</div>
</body>
</html>

这个示例中我们看到,如果要使用mxgraph,需要定义路径常量mxBasePath等于mxgraph核心资源的路径’../../src’,然后还要引用mxClient.js文件,在这之后我们就可以使用mxgraph了。
其中我把绘制的核心代码单独拿出来:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 新建一个mxgraph中的graph示例,graph可以理解为我们绘制图形的实例对象
var graph = new mxGraph(container);

//获取当前图层之上的父图层
var parent = graph.getDefaultParent();

// 每次新增图形,或者更新图形的时候必须要调用这个方法
graph.getModel().beginUpdate();
try
{
// 这条语句在图层中绘制出一个内容为'Hello'的矩形
// insertVertex()函数中依次传入的是父图层,当前图元的id,图元中的内容,定位x,定位y,宽w,高h,后面还可以添加参数为当前图源的样式,是否为相对位置
var v1 = graph.insertVertex(parent, null, 'Hello,', 20, 20, 80, 30);
var v2 = graph.insertVertex(parent, null, 'World!', 200, 150, 80, 30);
// 这条语句使用insertEdge,在图层中绘制出一个由v1指向v2的线。
var e1 = graph.insertEdge(parent, null, '', v1, v2);
} finally {
// 每次更新或者新增图形之后必须调用这个方法,所以这个方法需要在finally中执行
graph.getModel().endUpdate();
}

2.stylesheet.html

这个例子告诉我们如何修改图形的样式。分为两种方式,修改全局样式和修改单独某个图元的样式。
先看全局修改样式的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 修改矩形图元的默认样式
var style = [];
style[mxConstants.STYLE_SHAPE] = mxConstants.SHAPE_RECTANGLE;
style[mxConstants.STYLE_PERIMETER] = mxPerimeter.RectanglePerimeter;
style[mxConstants.STYLE_ROUNDED] = true;
style[mxConstants.STYLE_FILLCOLOR] = '#EEEEEE';
style[mxConstants.STYLE_FONTCOLOR] = '#774400';
style[mxConstants.STYLE_ALIGN] = mxConstants.ALIGN_CENTER;
style[mxConstants.STYLE_FONTSIZE] = '12';
graph.getStylesheet().putDefaultVertexStyle(style);

// 修改连线的默认样式
style = [];
style[mxConstants.STYLE_SHAPE] = mxConstants.SHAPE_CONNECTOR;
style[mxConstants.STYLE_STROKECOLOR] = '#6482B9';
style[mxConstants.STYLE_VERTICAL_ALIGN] = mxConstants.ALIGN_MIDDLE;
style[mxConstants.STYLE_EDGE] = mxEdgeStyle.ElbowConnector;
style[mxConstants.STYLE_FONTSIZE] = '10';
graph.getStylesheet().putDefaultEdgeStyle(style);

mxStylesheet类用于管理图形样式,通过 graph.getStylesheet() 可以获取当前图形的 mxStylesheet对象。mxStylesheet 对象的 style 属性也是一个对象,该对象默认情况下包含两个对象defaultVertexStyle、defaultEdgeStyle,修改这两个对象里的样式属性对所有线条/节点都生效。

我们看下如何单独修改某一个图元的样式。

1
2
3
4
5
6
7
8
9
10
11
var myStyle = []; // 定义一个自定义样式
style[mxConstants.STYLE_SHAPE] = mxConstants.SHAPE_RECTANGLE;
style[mxConstants.STYLE_STROKECOLOR] = '#6482B9';
style[mxConstants.STYLE_ALIGN] = mxConstants.ALIGN_CENTER;
style[mxConstants.STYLE_VERTICAL_ALIGN] = mxConstants.ALIGN_MIDDLE;
style[mxConstants.STYLE_FONTSIZE] = '10';

// 这里我们使用putCellStyle() 定义一个自己的样式myCellStyle
graph.getStylesheet().putCellStyle('myCellStyle', myStyle);
// 注意下面我们用了两种方式添加给一个单独的cell添加样式
var v1 = graph.insertVertex(parent, null, 'myCell', 100, 100, 50, 30, 'myCellStyle;FONTCOLOR=#999999')

如上面所示,我们可以使用putCellStyle()自己定义个自己的样式对象,在新建图元的时候当作参数使用,也可以同时直接添加你想要的样式。格式如下:

1
'stylename;key=value;'

分号前可以跟命名样式名称或者一个样式的 key、value 。对了,如果使用 [mxConstants.STYLE_IMAGE]: ‘path/image.png’这个样式属性的话,可以在节点上添加图片。

3.wrapping.html

这个例子告诉我们如何在节点(vertex)和连线(edge)的标签上使用HTML标签和字符换行,直接来看其中最核心的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
var graph = new mxGraph(container);

// 渲染的时候使用html标签
graph.setHtmlLabels(true);

// 在渲染线edge的时候 禁止使用页面内编辑
graph.isCellEditable = function(cell){
return !this.model.isEdge(cell);
};

var parent = graph.getDefaultParent();

graph.getModel().beginUpdate();
try{
var v1 = graph.insertVertex(parent, null, 'Cum Caesar vidisset, portum plenum esse, iuxta navigavit.',
20, 20, 100, 70, 'whiteSpace=wrap;');
var v2 = graph.insertVertex(parent, null, 'Cum Caesar vidisset, portum plenum esse, iuxta navigavit.',
220, 150, 80, 70, 'whiteSpace=wrap;');
var e1 = graph.insertEdge(parent, null, 'Cum Caesar vidisset, portum plenum esse, iuxta navigavit.',
v1, v2, 'whiteSpace=wrap;');
e1.geometry.width = 100;
} finally {
graph.getModel().endUpdate();
}

所以中核心的一代码为graph.setHtmlLabels(true),同时配合节点的样式’whiteSpace=wrap’就可以实现在图中换行。

4.htmlLabel.html

这个例子在节点(vertex)中添加了html代码,并用html关联了里面的状态:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
var graph = new mxGraph(container);

graph.setHtmlLabels(true);

// 创建一个保存状态的xml用户对象
var doc = mxUtils.createXmlDocument();
var obj = doc.createElement('UserObject');
obj.setAttribute('label', 'Hello, World!');
obj.setAttribute('checked', 'false');

// Adds optional caching for the HTML label
var cached = true;

if (cached){
// Ignores cached label in codec
mxCodecRegistry.getCodec(mxCell).exclude.push('div');

// Invalidates cached labels
graph.model.setValue = function(cell, value){
cell.div = null;
mxGraphModel.prototype.setValue.apply(this, arguments);
};
}

// 这里是重写了展示cell内部展现方法,改写为展示html标签的内容
// 这里的代码可以实现自定义cell内部的html结构
graph.convertValueToString = function(cell){
if (cached && cell.div != null){
// 使用缓存中的html
return cell.div;
} else if (mxUtils.isNode(cell.value) && cell.value.nodeName.toLowerCase() == 'userobject') {
// 返回一个div标签的 dom元素
var div = document.createElement('div');
div.innerHTML = cell.getAttribute('label');
mxUtils.br(div); // 添加一个换行
// 定义一个input checkbox元素
var checkbox = document.createElement('input');
checkbox.setAttribute('type', 'checkbox');
// 设置input的初始属性值
if (cell.getAttribute('checked') == 'true') {
checkbox.setAttribute('checked', 'checked');
checkbox.defaultChecked = true;
}

// 添加checkbox被点击之后的事件处理
mxEvent.addListener(checkbox, (mxClient.IS_QUIRKS) ? 'click' : 'change', function(evt) {
var elt = cell.value.cloneNode(true);
elt.setAttribute('checked', (checkbox.checked) ? 'true' : 'false');

graph.model.setValue(cell, elt);
});
// 在新建的div dom上添加input checkbox元素
div.appendChild(checkbox);

if (cached) { // 将新建的html添加到缓存中
// Caches label
cell.div = div;
}
return div;
}

return '';
};

// Overrides method to store a cell label in the model
var cellLabelChanged = graph.cellLabelChanged;
graph.cellLabelChanged = function(cell, newValue, autoSize)
{
if (mxUtils.isNode(cell.value) && cell.value.nodeName.toLowerCase() == 'userobject')
{
// Clones the value for correct undo/redo
var elt = cell.value.cloneNode(true);
elt.setAttribute('label', newValue);
newValue = elt;
}

cellLabelChanged.apply(this, arguments);
};

// Overrides method to create the editing value
var getEditingValue = graph.getEditingValue;
graph.getEditingValue = function(cell)
{
if (mxUtils.isNode(cell.value) && cell.value.nodeName.toLowerCase() == 'userobject')
{
return cell.getAttribute('label');
}
};

var parent = graph.getDefaultParent();
// 使用自己定义xml用户对象 新建vertex
graph.insertVertex(parent, null, obj, 20, 20, 80, 60);

上面这个代码包含了如何使用自己定义的html绘制节点(vertex)中的内容,且给其中的元素绑定事件。通过代码可以看出是需要同时新建一个xml对象和相应的dom元素。由此可以知道在mxgraph中每个图形都有其对应的xml数据结构。

但是上述中的代码还有几处我也没搞懂的地方,比如缓存处理、cellLabelChanged()和getEditingValue()的用法,之后还需要查阅资料,或者请教公司的小伙伴。

5.hierarchicallayout.html

这个示例是展示了绘图中的两种自动布局方法,分别是分级和有机布局算法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
var graph = new mxGraph(container);

// 在graph实例上新建这两种布局方法
var layout = new mxHierarchicalLayout(graph); // 分级布局
var organic = new mxFastOrganicLayout(graph); // 有机布局
organic.forceConstant = 120; // 这个值是有机布局节点的平均间距。默认是50

var parent = graph.getDefaultParent();

// 添加一个触发分级布局的按钮
var button = document.createElement('button');
mxUtils.write(button, 'Hierarchical');
mxEvent.addListener(button, 'click', function(evt){
layout.execute(parent);
});
document.body.appendChild(button);

// 添加一个触发有机算法布局的按钮
var button = document.createElement('button');
mxUtils.write(button, 'Organic');
mxEvent.addListener(button, 'click', function(evt){
organic.execute(parent);
});

document.body.appendChild(button);

graph.getModel().beginUpdate();
try
{
var v1 = graph.insertVertex(parent, null, '1', 0, 0, 80, 30);
var v2 = graph.insertVertex(parent, null, '2', 0, 0, 80, 30);
var v3 = graph.insertVertex(parent, null, '3', 0, 0, 80, 30);
var v4 = graph.insertVertex(parent, null, '4', 0, 0, 80, 30);
var v5 = graph.insertVertex(parent, null, '5', 0, 0, 80, 30);
var v6 = graph.insertVertex(parent, null, '6', 0, 0, 80, 30);
var v7 = graph.insertVertex(parent, null, '7', 0, 0, 80, 30);
var v8 = graph.insertVertex(parent, null, '8', 0, 0, 80, 30);
var v9 = graph.insertVertex(parent, null, '9', 0, 0, 80, 30);

var e1 = graph.insertEdge(parent, null, '', v1, v2);
var e2 = graph.insertEdge(parent, null, '', v1, v3);
var e3 = graph.insertEdge(parent, null, '', v3, v4);
var e4 = graph.insertEdge(parent, null, '', v2, v5);
var e5 = graph.insertEdge(parent, null, '', v1, v6);
var e6 = graph.insertEdge(parent, null, '', v2, v3);
var e7 = graph.insertEdge(parent, null, '', v6, v4);
var e8 = graph.insertEdge(parent, null, '', v6, v1);
var e9 = graph.insertEdge(parent, null, '', v6, v7);
var e10 = graph.insertEdge(parent, null, '', v7, v8);
var e11 = graph.insertEdge(parent, null, '', v7, v9);
var e12 = graph.insertEdge(parent, null, '', v7, v6);
var e13 = graph.insertEdge(parent, null, '', v7, v5);

// 添加完点线之后 默认使用分级布局
layout.execute(parent);
} finally {
graph.getModel().endUpdate();
}

if (mxClient.IS_QUIRKS) { // 样式的兼容处理
document.body.style.overflow = 'hidden';
new mxDivResizer(container);
}

这个例子比较简单,看注释就可以理解了。同时mxgraph中还有其他的布局算法。

6.layers.html

这个例子是使用多图层来放置细胞(cell),在动态绘图的过程中,或许可以使用这个来实现动态添加新的图层。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
// 在所给的container中用model新建graph
// 需要个root和两个layers
// 通过var layer = model.add(root, new mxCell())可以新增layer
var root = new mxCell();
var layer0 = root.insert(new mxCell());
var layer1 = root.insert(new mxCell());
var model = new mxGraphModel(root);

var graph = new mxGraph(container, model);
graph.setEnabled(false);

var parent = graph.getDefaultParent();
model.beginUpdate();
try{
// 分别在不同的图层中添加vertex和edge
var v1 = graph.insertVertex(layer1, null, 'Hello,', 20, 20, 80, 30, 'fillColor=#C0C0C0');
var v2 = graph.insertVertex(layer1, null, 'Hello,', 200, 20, 80, 30, 'fillColor=#C0C0C0');
var v3 = graph.insertVertex(layer0, null, 'World!', 110, 150, 80, 30);

var e1 = graph.insertEdge(layer1, null, '', v1, v3, 'strokeColor=#0C0C0C');
e1.geometry.points = [new mxPoint(60, 165)];

var e2 = graph.insertEdge(layer0, null, '', v2, v3);
e2.geometry.points = [new mxPoint(240, 165)];

var e3 = graph.insertEdge(layer0, null, '', v1, v2,
'edgeStyle=topToBottomEdgeStyle');
e3.geometry.points = [new mxPoint(150, 30)];

var e4 = graph.insertEdge(layer1, null, '', v2, v1,
'strokeColor=#0C0C0C;edgeStyle=topToBottomEdgeStyle');
e4.geometry.points = [new mxPoint(150, 40)];
} finally {
model.endUpdate();
}

// 新增layer 0 按钮,点击控制layer0的显示隐藏
document.body.appendChild(mxUtils.button('Layer 0', function(){
model.setVisible(layer0, !model.isVisible(layer0));
}));
// 同上
document.body.appendChild(mxUtils.button('Layer 1', function(){
model.setVisible(layer1, !model.isVisible(layer1));
}));

总结

上面知识简单介绍了几个跟公司需求有关的demo,其中还有有一些问题没有解决。比如如何绑定节点中的鼠标事件,如何将json数据转换为一个图形等等。还需要之后的努力。通过这几个星期的学习,清楚的认识到自己的能力还有较大的提升,同时英文真的是工程师很重要的一个技能。同时感觉到自己的对于问题的解决方案,还是缺少直觉。后面还需要继续的全面提高自己。

参考文章

mxGraph

mxGraph api specification

mxGraph用户手册中文版

mxGraph入门实例教程


作者简介: 宫晨光,人和未来大数据前端工程师。