GraphViz 简介

作者:Mihalis Tsoukalos

GraphViz 是一个用于操作图结构和生成图布局的工具集合。图可以是定向的或非定向的。GraphViz 提供图形化和命令行工具。Perl 接口也可用,但出于通用性考虑,此处不作介绍。本文也不讨论图形化工具。相反,本文重点介绍如何从命令行使用 GraphViz。

一些用户喜欢使用命令行工具而不是图形化工具,因为命令行工具可以与脚本语言一起使用。这意味着您可以创建脚本来创建动态图。动态图的一些可能用途包括

  • 跟踪路由表示数据包到达多个目的地,采用树的形式

  • 数据库表之间的关系图

  • 给老板的图形化报告,从 cron 作业自动通过电子邮件发送

  • 网站地图

  • UML 图

  • 基于 RPM 的系统上的 RPM 包依赖项

  • 源代码结构图

该软件包已编译为大多数 Linux 和 UNIX 发行版(请参阅“资源”)。它也可以以源代码形式提供,因此您可以自行编译。使用您喜欢的软件安装方法在您的系统上获取 GraphViz。

GraphViz 由以下程序和库组成。

  • dot 程序:一个用于绘制有向图的实用程序。它接受 dot 语言的输入。dot 语言可以定义三种类型的对象:图、节点和边。dot 使用 Sugiyama 风格(请参阅“资源”)的分层布局。

  • NEATO 程序:一个用于绘制无向图的实用程序。这种类型的图通常用于电信和计算机编程任务。NEATO 使用 Kamada-Kawai(请参阅“资源”)算法的实现来进行对称布局。

  • twopi 程序:一个用于使用圆形布局绘制图的实用程序。选择一个节点作为中心,其他节点以圆形模式放置在中心周围。如果一个节点连接到中心节点,则将其放置在距离 1 处。如果一个节点连接到直接连接到中心节点的节点,则将其放置在距离 2 处,依此类推。

  • dotty、tcldot 和 lefty:三个图形化程序。dotty 是一个用 lefty 编写的 X Window 系统的可定制界面。tcldot 是一个用 Tcl 7 编写的可定制图形界面。lefty 是一个用于技术图片的图形编辑器。

  • libgraph 和 libagraph:绘图库。它们的存在意味着应用程序可以将 GraphViz 用作库而不是软件工具。

基本图形绘制

必须展示执行 dot、NEATO 和 twopi 程序的方式。这些程序具有相似的命令行参数。运行 dot、NEATO 或 twopi 的通用方法如下

toolname -Tps filename -o output.ps

此命令执行工具(toolname),声明输出应为 PostScript(使用 filename 作为输入)并生成 (-o) output.ps 文件。此处不讨论其他可能的命令行参数,但您可以查看手册页或 GraphViz 网站(请参阅“资源”)以获取更多信息。

一些简单的 GraphViz 图

图 G(V,E) 是顶点 V 或节点和边 E 的有限非空集合。如果边 E 具有 (a,b) 的形式,是顶点的有序对,那么我们有一个有向图。如果边 E 是顶点的无序对,我们有一个无向图(请参阅“资源”)。

要使用 dot 制作二叉树,我们应该了解以下理论。没有循环的有向图称为有向无环图。如果一个有向无环图满足以下属性,则它是一棵树

  • 存在一个且仅存在一个顶点(根),没有边进入

  • 除了根顶点之外,每个顶点只有一个进入边。

  • 从根到每个顶点都存在唯一路径。

如果树中每个顶点的子节点都是有序的,则该树称为有序树。二叉树是一棵有序树,使得

  • 顶点的每个子节点都被区分为左子节点或右子节点。

  • 每个顶点最多可以有一个左子节点和一个右子节点。

shape 参数的 record 参数是此示例中的关键参数。此外,label 参数有一些指针(命名为 f0、f1 和 f2 - dot 语言代码中的 <f0>、<f1> 和 <f2>)来声明我们要连接的记录的特定部分。我们使用 | 符号来分隔指针。请注意,在定义每个节点之后,我们使用 nodename: 指针来表示我们要连接的记录部分。有关输出,请参见图 1,有关 dot 源代码,请参见列表 1。

An Introduction to GraphViz

图 1. 二叉树

列表 1. 图 1 的 Dot 源代码

digraph G
{
        node [shape = record];

        node0 [ label ="<f0> | <f1> J | <f2> "];
        node1 [ label ="<f0> | <f1> E | <f2> "];
        node4 [ label ="<f0> | <f1> C | <f2> "];
        node6 [ label ="<f0> | <f1> I | <f2> "];
        node2 [ label ="<f0> | <f1> U | <f2> "];
        node5 [ label ="<f0> | <f1> N | <f2> "];
        node9 [ label ="<f0> | <f1> Y | <f2> "];
        node8 [ label ="<f0> | <f1> W | <f2> "];
        node10 [ label ="<f0> | <f1> Z | <f2> "];
        node7 [ label ="<f0> | <f1> A | <f2> "];
        node3 [ label ="<f0> | <f1> G | <f2> "];


        "node0":f0 -> "node1":f1;
        "node0":f2 -> "node2":f1;

        "node1":f0 -> "node4":f1;
        "node1":f2 -> "node6":f1;
        "node4":f0 -> "node7":f1;
        "node4":f2 -> "node3":f1;

        "node2":f0 -> "node5":f1;
        "node2":f2 -> "node9":f1;

        "node9":f0 -> "node8":f1;
        "node9":f2 -> "node10":f1;
}

哈希表是一个数组,它保存指向由哈希值索引的条目的指针。这样的条目可以是内存地址、保存所需信息的数据库记录、文件中作为所需记录起点的特定位置等等。哈希表用于易于查找目的的搜索。例如,它们通常用作 DBMS 系统和编译器中的索引。

rotate=90下面的列表中的命令返回一个横向输出页面。请注意,label 参数内的 {} 告诉 dot 语言将记录部分从左到右连接,而不是一个在另一个之上。以这种方式绘制哈希表比使用图形化工具更容易。

rankdir=LR命令是列表 1 中的关键命令。它告诉 dot 语言从左到右放置节点。有关输出,请参见图 2,有关 dot 源代码,请参见列表 2。

An Introduction to GraphViz

图 2. 哈希表

列表 2. 图 2 的 Dot 源代码

digraph G
{
        rankdir = LR;
        node [shape=record, width=.1, height=.1];
        rotate=90;

        node0 [label = "<p0> | <p1> | <p2> | <p3> \\
                 | <p4> | | ", height = 3];

        node[ width=2 ];
        node1 [label = "{<e> r0 | 123 | <p> }" ];
        node2 [label = "{<e> r10 | 13 | <p> }" ];
        node3 [label = "{<e> r11 | 23 | <p> }" ];
        node4 [label = "{<e> r12 | 326 | <p> }" ];
        node5 [label = "{<e> r13 | 1f3 | <p> }" ];
        node6 [label = "{<e> r20 | 123 | <p> }" ];
        node7 [label = "{<e> r40 | b23 | <p> }" ];
        node8 [label = "{<e> r41 | 12f | <p> }" ];
        node9 [label = "{<e> r42 | 1d3 | <p> }" ];

        node0:p0 -> node1:e;
        node0:p1 -> node2:e;
        node2:p -> node3:e;
        node3:p -> node4:e;
        node4:p -> node5:e;

        node0:p2 -> node6:e;
        node0:p4 -> node7:e;
        node7:p -> node8:e;
        node8:p -> node9:e;

}

您可以将前面的任何一个示例与 twopi 一起使用。使用 twopi 的主要原因是它允许您轻松定义和更改图的中心项。这可以使用 center 命令,后跟文件开头的节点名称来完成。有关输出,请参见图 3,有关 twopi 源代码,请参见列表 3。

An Introduction to GraphViz

图 3. twopi 图

列表 3. 图 3 的源代码

digraph G
{
        center = v21;

        center -> v11;
        center -> v12;
        center -> v13;

        v11 -> v21;
        v11 -> v22;
        v11 -> v23;

        v21 -> v22;
        v22 -> v23;
        v23 -> v21;

        v21 -> v31;
        v21 -> v32;
        v21 -> v33;

        v32 -> v41;
        v32 -> v42;
        v33 -> v43;
}

绘制月度日历

在这里,我们将逐步完成使用小型 Perl 脚本创建月度日历的简单任务。有关脚本的命令行参数的更多信息,请参见下面的相关 Perl 代码。

您可以尝试输出的大小、颜色和形状来自定义日历并使其看起来符合您的要求。这个小例子只是简单地展示了脚本语言与智能工具结合可以做什么。有关输出,请参见图 4,有关 Perl 源代码,请参见列表 4。

An Introduction to GraphViz

图 4. 月度日历

列表 4. 图 4 的 Perl 源代码

#!/usr/bin/perl -w

# $Id: MYdot.pl,v 3.1 2004/02/05 22:03:32 mtsouk Exp mtsouk $
#
# A perl script to generate monthly
# calendars using dot.
#
# Mihalis Tsoukalos 2004
#  * * * * * * * * * * * * * * * * * * * * * * * *

# Command line arguments
# MYdot.pl MonthName MonthYear NofDays StartingDay
#
# MonthName: This is given by the user.
# Year: This is given by the user.
# NofDays: The number of the month's days.
# Month Starting Day: 0 for Sunday etc.

die <<Thanatos unless @ARGV;
usage:
    $0 MonthName MonthYear NofDays StartingDay
    MonthName: The name of the month in the output.
    Year: The Year to appear in the output.
    NofDays: The number of the month's days.
    Month first Day: 0 for Sunday, 1 for Monday etc.
Thanatos

if ( @ARGV != 4 )
{
    die <<Thanatos
    usage info:
        Please use exactly 4 arguments!
Thanatos
}

# Get the values of the command line arguments
($MonthName, $Year, $NofDays, $SDay) = @ARGV;


# This will tell how many weeks exist in the month
$NofWeeks = (($NofDays + $SDay) / 7);
if ( $NofWeeks > int($NofWeeks) )
{
    $NofWeeks = int($NofWeeks) + 1;
}


my $filename = $MonthName.".dot";
open(OUTPUT, "> $filename " ) ||
    die "Cannot create $filename: $!\n";

$temp = $SDay;

#
# Fix the first week
#
while ( $temp > 0 )
{
    $month[$temp] = "em".$temp;
    $temp--;
}

for ( $i = 0; $i<$NofDays; $i++ )
{
    $month[$i+$SDay+1] = "day".($i+1)." ";
}

print OUTPUT <<PRE;
digraph G
{

    subgraph weekdays
    {
        node [ style=filled, color=lightgray, \\
            height=.60, width=1.15];
        sun->mon->tue->wed->thu->fri->sat;
        label = "WeekDays";
    }

    "$MonthName $Year" [shape=Msquare];
    "$MonthName $Year" -> sun;
    "$MonthName $Year" -> $month[1];
    "$MonthName $Year" -> $month[8];
    "$MonthName $Year" -> $month[15];
    "$MonthName $Year" -> $month[22];
PRE


#
# All months have at least 4 weeks (not full).
# Checking is being done here to see
# if the current months has more that 4 weeks.
#
if (defined $month[29] )
{
print OUTPUT <<WEEK5;
    "$MonthName $Year" -> $month[29];
WEEK5
}

#
# The maximum number of weeks a month can have is 6
#
if( defined $month[36] )
{
print OUTPUT <<WEEK6;
    "$MonthName $Year" -> $month[36];
WEEK6
}

for ( $i=0; $i<$NofWeeks; $i++ )
{
    print OUTPUT "subgraph week".($i+1);
    print OUTPUT "{";
    print OUTPUT "node [ height=.60, width=.85];";

    for ( $j=1; $j<7; $j++)
    {
        if (defined $month[$i*7+$j])
        {
            print OUTPUT $month[$i*7+$j];
            if (defined $month[$i*7+$j+1])
            {
                print OUTPUT " -> ";
            }
        }
    }

    #
    # The 7th day of each week is a special case
    # as it does not have a -> after.
    #
    if (defined $month[$i*7+7] )
    {
        print OUTPUT $month[$i*7+7].";\n";
    }
    print OUTPUT "}\n";
}

# Type the empty days into the output file
$temp = $SDay;
while ( $temp > 0 )
{
    print OUTPUT "em".$temp;
    print OUTPUT " [ shape=box, label=\"\" ];";
    print OUTPUT "\n";
    $temp--;
}

# Type the days into the output file
for ( $i=1; $i <= $NofDays; $i++ )
{
    print OUTPUT "\t";
    print OUTPUT "day".$i;
    print OUTPUT " [shape=box, label=\"".$i."\"];";
    print OUTPUT "\n";
}

print OUTPUT <<ENDING;

    sun [shape=egg, label="Sunday"];
    mon [shape=egg, label="Monday"];
    tue [shape=egg, label="Tuesday"];
    wed [shape=egg, label="Wednesday"];
    thu [shape=egg, label="Thursday"];
    fri [shape=egg, label="Friday"];
    sat [shape=egg, label="Saturday"];
}
ENDING

close(OUTPUT) ||
    die "Cannot close $filename: $!\n";

exit 0;

更复杂的图形绘制

现在我们将看看您可以使用 GraphViz 完成的一些更复杂的示例。我们首先使用 NEATO 构建实体关系图(用于 DBMS 系统)。实体关系图是表示实体集、属性和关系的图。通常的做法是将实体集表示为矩形,将属性表示为椭圆形,将关系表示为菱形。GraphViz 可以轻松地用于构建此类图,如下例所示。

这里要记住的关键点是在不同类型的元素之间更改形状——矩形、椭圆形和菱形。这可以使用 shape 属性来完成。有关输出,请参见图 5,有关 NEATO 源代码,请参见列表 5。

An Introduction to GraphViz

图 5. 实体关系图

列表 5. 图 5 的源代码

graph G
{
    rotate=90;

    id[label="id"];
    email[label="email"];
    name[label="name"];

    uname[label="Name"];
    address[label="Address"];

    node[shape=rectangle];
    university[label="University Department"];
    student[label="Student"];

    StUnDe[shape=diamond];

    university -- StUnDe [label="1", len=3];
    StUnDe -- student [label="n", len=2];

    student -- id;
    student -- name;
    student -- email;

    university -- uname;
    university -- address;
}

接下来是使用 NEATO 的边上带有成本的无向图的示例。这是一个有用的示例,因为这种类型的图可以用于表示网络结构,而成本值有助于定义本地或广域网络的最佳路由表。它应该有助于轻松理解网络拓扑。此图取自 计算机算法的设计和分析 (请参阅“资源”)的第 174 页。有关输出,请参见图 6,有关 NEATO 源代码,请参见列表 6。

An Introduction to GraphViz

图 6. 带有成本的无向图

列表 6. 图 6 的源代码

graph G
{
        edge [len=2];

        V1 -- V2 [label="20"];
        V2 -- V3 [label="15"];
        V3 -- V4 [label="3"];
        V4 -- V5 [label="17"];
        V5 -- V6 [label="28"];
        V6 -- V1 [label="23"];

        V7 -- V1 [label="1"];
        V7 -- V2 [label="4"];
        V7 -- V3 [label="9"];
        V7 -- V4 [label="16"];
        V7 -- V5 [label="25"];
        V7 -- V6 [label="36"];
}

如果您首先放置六个 V7 连接,则输出效果不佳;至少,它与原始图形不相似。当然,信息量是相同的。您可以对文件进行一些更改,看看输出效果如何。

制作 RCS 修订树

以下示例展示了 RCS 修订树结构的图形表示。同样,Perl 是首选的编程语言。您可以想象,Perl 脚本并不完美,因为它没有涵盖所有可能的修订树结构。在此示例中,展示了小型修订树以进行说明。对于复杂的修订树,在编写 Perl 脚本时必须更多地考虑修订树的特殊性。

脚本背后的基本思想是首先从 rlog 命令获取我们想要的输出。rlog 命令返回有关文件更改历史记录的大量信息,因此我们必须 grep 输出以获取所需的数据。rlog 实用程序包含在 RCS 修订控制系统中。

脚本中的关键点是通过修订号的第一部分分隔修订分支。这样,每个修订分支都有自己的子图。

请注意,您必须在修订名称周围加上引号。否则,输出将无法正确显示。有关输出,请参见图 7,有关 Perl 源代码,请参见列表 7。

An Introduction to GraphViz

图 7. RCS 修订树图

列表 7. 图 7 的 Perl 源代码

#!/usr/bin/perl -w

# $Id: RCS.pl,v 1.2 2004/02/05 12:21:40 mtsouk Exp mtsouk $
#
# Command line arguments
# program_name.pl file_with_RCS_info

use strict;

my $filename="";
my $COMMAND="";
my %revision=();
# Change that according to your system
my $RLOG="/usr/bin/rlog";
my $rev=0;
my $count=0;
my %branch=();
my $date=0;

die <<Thanatos unless @ARGV;
usage:
   $0 file_with_RCS_info
Thanatos

if ( @ARGV != 1 )
{
   die <<Thanatos
      usage info:
         Please use exactly 1 argument!
Thanatos
}

# Get the file name
($filename) = @ARGV;

$COMMAND = "$RLOG $filename |";
#
# Do not forger to change the path
# of the grep command
#
$COMMAND = $COMMAND." /bin/grep ^revision -A 1 ";
open (RCSINFO, "$COMMAND |")
   || die "Cannot run the ".$COMMAND.": $!\n";

my $line = "";
my $connect="";
while ($line = <RCSINFO>)
{
   if ( $line =~ /^revision/ )
   {
      $rev = (split " ", $line)[1];
      $count = (split /\./, $rev)[0];
      if ( defined $branch{$count} )
      {
         my $number = $branch{$count};
         $number++;
         $branch{$count} = $number;
      }
      else
      {
         $branch{$count}=1;
         $connect.="\"$filename\" -> \"rev$rev\";";
      }
      $line = <RCSINFO>;
      if ( $line =~ /^date:/ )
      {
         $date = (split / /, $line)[1];
         $revision{$rev} = $date;
      }
   }
}

close(RCSINFO)
   || die "Cannot close RCSINFO: $!\n";

my $FILE = $filename.".dot";
open (OUTPUT, "> $FILE" )
   || die "Cannot create file: $!\n";

print OUTPUT <<START;
digraph G
{
   "$filename" [shape=Msquare];
   node [ style=filled, color=lightgray];
   node [ height=.50, width=.65];
START

#
# Now we will process the output of the rlog command
# We want to get the revision number,
# the date and the username
#
my $k="";
foreach $k (sort keys %revision)
{
   print "$k => $revision{$k}\n";
   print OUTPUT <<DATA;
   "rev$k" [shape=egg, label="$k\\n$revision{$k}"];
DATA
}

my $ll="";
foreach $ll (keys %branch)
{
   print OUTPUT "\t";
   foreach $k (sort keys %revision)
   {
      my $major = (split /\./, $k)[0];
      if ( $ll == $major )
      {
         my $counter = $branch{$ll};
         $counter--;
         $branch{$ll} = $counter;
         print OUTPUT "\"rev".$k."\"";
         if ( $counter > 0 )
         {
            print OUTPUT "  -> ";
         }
         else
         {
            print OUTPUT ";\n";
         }
      }
   }
}

print OUTPUT <<END;
   \t$connect
}
END

close (OUTPUT)
   || die "Cannot close file: $!\n";

exit 0;

GraphViz 中的目录结构

我们的下一个示例是更常用的图形:操作系统目录结构的图形表示。请记住,由于页面尺寸限制,可以呈现的目录数量受到限制。此 Perl 脚本已用少量目录进行过测试。此外,目录结构本身是输出质量的最重要因素。意思是,如果我们可以将目录结构想象成树结构,那么如果树的最大级别是 4,并且树的最大级别是 10,而大多数分支的深度为 3,则可能会有很大的不同。

请注意,每个框都不显示完整路径名,仅显示目录名称的最后一部分。您可以通过跟踪链接找到完整路径名。请查看源代码以了解脚本的工作原理。请记住,源代码尚未完全测试;它只是用来展示 dot 语言在脚本语言的少量帮助下可以做什么。有关输出,请参见图 8,有关 Perl 源代码,请参见列表 8。

An Introduction to GraphViz

图 8. 目录结构图

列表 8. 图 8 的 Perl 源代码

#!/usr/bin/perl -w

# $Id: DIR.pl,v 1.3 2004/02/06 15:18:13 mtsouk Exp mtsouk $
#
# Please note that this is alpha code
#
# Command line arguments
# program_name.pl directory

use strict;

my $directory="";
my $COMMAND="";
my %DIRECTORIES=();

die <<Thanatos unless @ARGV;
usage:
   $0 directory
Thanatos

if ( @ARGV != 1 )
{
   die <<Thanatos
      usage info:
         Please use exactly 1 argument!
Thanatos
}

# Get the file name
($directory) = @ARGV;

$COMMAND = "/usr/bin/find $directory -type d | ";
open (INPUT, "$COMMAND")
   || die "Cannot run the ".$COMMAND.": $!\n";

#
# The reason for putting OUTPUT in front of the
# directory name is that we
# can have . as directory name
#
my $OUTPUT="OUTPUT$directory.dot";
$OUTPUT =~ s/\//-/g;
open (OUTPUT, "> $OUTPUT")
   || die "Cannot create output file $OUTPUT: $!\n";

print OUTPUT <<START;
digraph G
{
   rotate=90;
   nodesep=.05;
   node[height=.05, shape=record, fontsize=5];
START

# Make nodes for the command line argument directory
my @split = split /\//, $directory;
my $key="";
my $prev=undef;
for $key (@split)
{
    my $KEY=$key;
    $key =~ s/[^[a-zA-Z0-9]/_/g;
    $key = $prev."_".$key;
    $prev = $key;
    print OUTPUT "\t".$prev;
    print OUTPUT " [shape=box, label=\"$KEY\"];";
    print OUTPUT "\n";
}

my $lastpart = "";
while (<INPUT>)
{
   chomp;
   my $orig=$_;

   # Get the right label
   my @split = split /\//, $_;
   $lastpart = pop @split;

   $_ =~ s/\//_/g;
   #
   # The _ is accepted as a valid node character
   # . , + - are not accepted
   #
   $_ =~ s/[^a-zA-Z0-9]/_/g;
   my @split = split /_/, $_;
   print OUTPUT "\t_".$_;
   print OUTPUT " [shape=box,label=\"$lastpart\"];";
   print OUTPUT "\n";
   $DIRECTORIES{$orig}=0;
}

my $subdir="";
my %TEMP=();
foreach $key ( sort keys %DIRECTORIES )
{
   print "KEY: $key\n";
   my @split = split /\//, $key;
   my $prev = undef;
   for $subdir (@split)
   {
      $subdir =~ s/[^a-zA-Z0-9_]/_/g;
      my $next = $prev."_".$subdir;
      # print "NEXT: $next\n";
      if ( !defined($prev) )
      {
         $prev = $next;
         next;
      }
      my $val = "$prev->$next;\n";
      # print "VAL: $val\n";
      if ( !defined( $TEMP{$val} ))
      {
         print OUTPUT "$prev->$next;\n";
      }
      $prev .= "_".$subdir;
      $TEMP{$val}=1;
   }
}

close(INPUT)
   || die "Cannot close input file: $!\n";


print OUTPUT <<END;
}
END

close (OUTPUT)
   || die "Cannot close file: $!\n";

exit 0;

结论

使用 dot、twopi 和 NEATO 创建图形并不困难。这三个实用程序是根据 UNIX 哲学制作的:它们易于使用,它们很好地完成一件事情,并且使用它们的唯一要求是一个简单的文本编辑器。

也存在使用 dot、twopi 和 NEATO 语言制作图形的图形化工具,但我仍然更喜欢命令行。所提供的工具既可以用于简单图形,也可以用于复杂图形。

我要感谢 Nikos Platis 校对本文。

资源

GraphViz 开发网站

GraphViz 官方网站

“用于可视化理解分层系统结构的方法”。Sugiyama, K., Tagawa, S. 和 Toda, M. 人和控制论。 IEEE Trans. Systems,1981 年 2 月。

绘制有向图的技术。Gansner, E. R., Koutsofios, E., North, S. C. 和 Vo, K. IEEE Trans. Software Engineering,1993 年 5 月。

“用于绘制一般无向图的算法”。Kamada, T. 和 Kawai, S. 信息处理快报,1989 年 4 月。

计算机算法的设计和分析。 Aho, Hopcroft 和 Ullman。Addison Wesley,1974 年。

Mihalis Tsoukalos 与他的妻子 Eugenia 一起住在希腊,并担任高中教师。此前,他曾担任 UNIX 系统管理员、Oracle DBA、UNIX 程序员和 PL/SQL 程序员。他拥有伦敦大学学院的数学学士学位和信息技术硕士学位。可以通过 mtsouk@freemail.gr 联系 Mihalis。

加载 Disqus 评论