编写布局插件
要创建一个名为 xxx
的新布局插件,首先需要提供两个函数:xxx_layout
和 xxx_cleanup
。这些函数的语义将在下面描述。
布局
void xxx_layout(Agraph_t * g)
初始化图。
-
如果算法将使用通用的边路由代码,它应该调用
setEdgeType (g, ...);
。 -
对于每个节点,调用
common_init_node
和gv_nodesize
。 -
如果算法将使用
spline_edges()
来路由边,节点坐标需要存储在ND_pos
中,因此需要在这里分配它。这以及上面提到的两个调用都由对neato_init_node()
的调用来处理。 -
对于每条边,调用
common_init_edge
。 -
算法应该分配它需要的任何其他数据结构。这可能涉及
A*info_t
字段中的字段。此外,这些字段中的每一个都包含一个void* alg;
子字段,算法可以使用它存储附加数据。一旦我们迁移到 cgraph,这将全部被替换为算法特定的记录。 -
对图进行布局。完成后,每个节点都应该将其坐标存储在
ND_coord_i(n)
中的点,每个边都应该在其ED_spl(e)
中描述其布局。(注意:从 2.21 版本开始,ND_coord_i
已被ND_coord
替换,现在它们是浮点坐标。)
要添加边,有 3 个可用的函数
spline_edges1 (Agraph_t*, int edgeType)
假设节点坐标存储在ND_coord_i
中,并且GD_bb
已设置。对于每条边,此函数构造适当的数据并将其存储在ED_spl
中。spline_edges0 (Agraph_t*)
假设节点坐标存储在ND_pos
中,并且GD_bb
已设置。此函数使用 ratio 属性(如果设置),将ND_pos
中的值复制到ND_coord_i
(从英寸转换为点);并使用setEdgeType()
指定的边类型调用spline_edges1
。spline_edges (Agraph_t*)
假设节点坐标存储在ND_pos
中。此函数计算 g 的边界框并将其存储在GD_bb
中,然后调用spline_edges0()
。
如果算法只适用于连通分量,代码可以使用 pack 库来获取分量,分别对其进行布局,并根据用户规格将它们打包在一起。下面给出了一个典型的模式。可以查看 twopi
、circo
、neato
或 fdp
的代码以获取更详细的示例。
Agraph_t **ccs;
Agraph_t *sg;
Agnode_t *c = NULL;
int ncc;
int i;
ccs = ccomps(g, &ncc, 0);
if (ncc == 1) {
/* layout nodes of g */
adjustNodes(g); /* if you need to remove overlaps */
spline_edges(g); /* generic edge routing code */
} else {
pack_info pinfo;
pack_mode pmode = getPackMode(g, l_node);
for (i = 0; i < ncc; i++) {
sg = ccs[i];
/* layout sg */
adjustNodes(sg); /* if you need to remove overlaps */
}
spline_edges(g); /* generic edge routing */
/* initialize packing info, e.g. */
pinfo.margin = getPack(g, CL_OFFSET, CL_OFFSET);
pinfo.doSplines = 1;
pinfo.mode = pmode;
pinfo.fixed = 0;
packSubgraphs(ncc, ccs, g, &pinfo);
}
for (i = 0; i < ncc; i++) {
agdelete(g, ccs[i]);
}
free(ccs);
如果依赖于仅在根图中设置的属性,则在布局子图时要小心。使用连通分量,边可以在打包之前(如上)或在分量打包之后(参见 circo)与每个分量一起添加。
最好检查图有 0 个或 1 个节点,或者没有边的平凡情况。
在 xxx_layout
结束时,调用
dotneato_postprocess(g);
以下模板在大多数情况下都会起作用,忽略了处理断开连接的图和删除节点重叠的问题
static void
xxx_init_node(node_t * n)
{
neato_init_node(n);
/* add algorithm-specific data, if desired */
}
static void
xxx_init_edge(edge_t * e)
{
common_init_edge(e);
/* add algorithm-specific data, if desired */
}
static void
xxx_init_node_edge(graph_t * g)
{
node_t *n;
edge_t *e;
for (n = agfstnode(g); n; n = agnxtnode(g, n)) {
xxx_init_node(n);
}
for (n = agfstnode(g); n; n = agnxtnode(g, n)) {
for (e = agfstout(g, n); e; e = agnxtout(g, e)){
xxx_init_edge(e);
}
}
}
void
xxx_layout (Agraph_t* g)
{
xxx_init_node_edge(g);
/* Set ND_pos(n) for each node n */
spline_edges(g);
dotneato_postprocess(g);
}
清理
void xxx_cleanup(Agraph_t * g)
释放布局中分配的任何资源。
完成对每个节点和边的 gv_cleanup_node
和 gv_cleanup_edge
的调用。这将清理 spline 标签、ND_pos
、形状并将 A*info_t
归零,因此这些必须最后发生,但如果需要,可以作为显式的 xxx_cleanup_node
和 xxx_cleanup_edge
的一部分。
最后,你应该执行
if (g != g->root) memset(&(g->u), 0, sizeof(Agraphinfo_t));
这对于图再次进行布局是必要的,因为布局代码假设此结构是干净的。
libgvc
对根图进行最后的清理,释放任何绘制内容,释放其标签,并将根图的 Agraphinfo_t
归零。
以下模板在大多数情况下都会起作用
static void xxx_cleanup_graph(Agraph_t * g)
{
/* Free any algorithm-specific data attached to the graph */
if (g != g->root) memset(&(g->u), 0, sizeof(Agraphinfo_t));
}
static void xxx_cleanup_edge (Agedge_t* e)
{
/* Free any algorithm-specific data attached to the edge */
gv_cleanup_edge(e);
}
static void xxx_cleanup_node (Agnode_t* n)
{
/* Free any algorithm-specific data attached to the node */
gv_cleanup_node(e);
}
void xxx_cleanup(Agraph_t * g)
{
Agnode_t *n;
Agedge_t *e;
for (n = agfstnode(g); n; n = agnxtnode(g, n)) {
for (e = agfstout(g, n); e; e = agnxtout(g, e)) {
xxx_cleanup_edge(e);
}
xxx_cleanup_node(n);
}
xxx_cleanup_graph(g);
}
大多数布局使用类似于 neato
的辅助例程,因此入口点可以添加到 plugin/neato_layout
中。
添加到 gvlayout_neato_layout.c
gvlayout_engine_t xxxgen_engine = {
xxx_layout,
xxx_cleanup,
};
以及该文件中的 gvlayout_neato_types
和 layout_type
中的新枚举 LAYOUT_XXX
的行
{LAYOUT_XXX, "xxx", 0, &xxxgen_engine, &neatogen_features},
LAYOUT_XXX
以上允许新布局 piggyback 在 neato
插件之上,但需要重建插件。通常,用户可以(并且可能应该)完全独立地构建布局插件。
为此,在编写 xxx_layout
和 xxx_cleanup
之后,需要
-
添加类型和数据结构
typedef enum { LAYOUT_XXX } layout_type; static gvlayout_features_t xxxgen_features = { 0 }; gvlayout_engine_t xxxgen_engine = { xxx_layout, xxx_cleanup, }; static gvplugin_installed_t gvlayout_xxx_types[] = { {LAYOUT_XXX, "xxx", 0, &xxxgen_engine, &xxxgen_features}, {0, NULL, 0, NULL, NULL} }; static gvplugin_api_t apis[] = { {API_layout, &gvlayout_xxx_types}, {(api_t)0, 0}, }; gvplugin_library_t gvplugin_xxx_layout_LTX_library = { "xxx_layout", apis };
-
将所有这些组合成一个动态库,其名称包含字符串
gvplugin_
,并将该库安装在与其他 Graphviz 插件相同的目录中。例如,在 Linux 系统上,dot 布局插件位于库libgvplugin_dot_layout.so
中。 -
运行
dot -c
以重新生成配置文件。
注意
- 可以在
gvlayout_xxx_types
中添加额外的布局作为额外的行。 - 显然,大多数名称和字符串可以是任意的。一个约束是
gvplugin_library_t
类型的外部标识符必须以_LTX_library
结尾。此外,gvlayout_xxx_types
中每个条目中的字符串xxx
是用于标识布局算法的名称,因此需要与任何其他布局名称不同。 - 布局算法的功能目前仅限于一个位标志,并且唯一支持的标志是
LAYOUT_USES_RANKDIR
,它使布局能够使用rankdir
属性。
需要对任何静态了解布局算法的应用程序进行更改。
Automake 配置
如果您想将您的代码集成到 Graphviz 软件中并使用其构建系统,请按照以下说明进行操作。您当然可以使用自己的构建软件来构建和安装您的插件。
- 将您的软件放在
lib/xxxgen
中,并将上面描述的钩子添加到gvlayout_neato_layout.c
中。 - 在
lib/xxxgen
中,提供一个Makefile.am
(基于lib/fdpgen/Makefile.am
等简单示例)。 - 在
lib/Makefile.am
中,将xxxgen
添加到SUBDIRS
中。 - 在
configure.ac
中,将lib/xxxgen/Makefile
添加到AC_CONFIG_FILES
中。 - 在
lib/plugin/neato_layout/Makefile.am
中,在libgvplugin_neato_layout_C_la_LIBADD
中插入$(top_builddir)/lib/xxxgen/libxxxgen_C.la
。 - 请记住运行
autogen.sh
,因为单独的configure
可能无法正确猜测。
这也假设您的系统上安装了各种 automake 工具的良好版本。