← На главную

Рисуем красивые графы при помощи Graphviz

Есть в юниксах такая полезная утилита под названием Graphviz, предназначенная для рисования графов. В этом посте я приведу пример ее использования. Тем, кто с ГрафВизом уже знаком, будет не интересно, лучше почитайте про Dracula. Остальных же, надеюсь, данный пост подтолкнет к более внимательному изучению данной программы.

Идея простая. Есть конфиг (файл .gv), с помощью которого дается описание графа. Конфиг этот примерно такого содержания:

digraph G { "A" -> "B"; "B" -> "C"; "A" -> "C"; }

Graphviz принимает такой конфиг на входе, а на выходе дает png|jpg|gif|svg файл с изображением графа. При желании можно дать более подробное описание графа. Например, указать форму и цвет вершин, толщину и направленность рёбер и так далее. Останавливаться на этом я не буду, в man-pages все детально расписано.

В состав Graphviz входит несколько программ, которые почему-то называются «фильтрами». С их помощью один и тот же граф можно нарисовать разными способами. Список и краткое описание программ:

dot - filter for drawing directed graphs neato - filter for drawing undirected graphs twopi - filter for radial layouts of graphs circo - filter for circular layout of graphs fdp - filter for drawing undirected graphs sfdp - filter for drawing large undirected graphs

Примеры графов, нарисованных с их помощью, вы можете посмотреть на официальном сайте программы.

Давайте сделаем вид, как будто вы почитали маны и полистали сайт. Отлично, теперь можно попробовать решить с помощью графвиза какую-нибудь задачку.

Представим, что у нас есть несколько компаний, а у этих компаний есть клиенты. По каким-то причинам мы можем отслеживать, к какой компании обращается клиент, если он отказывается от услуг другой компании. В случае, скажем, с хостингом, это действительно не сложно сделать. Задача – построить граф, изображающий перемещения клиентов от компании к компании.

Вот скрипт, генерирующий .gv файл:

#!/usr/bin/perl # gen-graph.pl # (c) Alexandr Alexeev 2010, http://eax.me/ use strict; use List::Util qw/max shuffle/; use List::MoreUtils qw/uniq/; use DBI; use constant MIN_PERCENT => 5; my %data; # {from}{to} = cnt my @sources; # генерируем легко различимые и не слишком темные цвета my @colors; for my $c(qw/8 D A 9 F 6/) { for my $m(qw/FF0000 00FF00 00FFFF FF00FF FFFF00/) { my $t = $m; $t =~ s/F/$c/g; push @colors, $t; } } # забираем данные из БД my $db = DBI->connect( "dbi:mysql:database:localhost", "user", "password", { PrintError => 0, RaiseError => 0 } ) or die "ERROR: $!\n"; my $query = qq{ select `from`, `to`, `count` from logs where month='201009'; }; my $res = $db->prepare($query); $res->execute() or die "Query failed:\n\n $query"; # начало описания графа print "digraph G {\n"; print qq{ nodesep=2; mindist=2; }; print "\n"; # максимум клиентов перешло my $max_cnt = 1; # плюс и минус число клиентов для компании my %plus; my %minus; while (my ($from, $to, $cnt) = $res->fetchrow_array()) { $data{$from}{$to} = $cnt; $plus{$to} += $cnt; $minus{$from} += $cnt; $max_cnt = $cnt > $max_cnt ? $cnt : $max_cnt; push @sources, $from; push @sources, $to; } # shuffle для того, чтобы можно было # перерисовать граф немного по-другому @sources = shuffle uniq @sources; my $i; for my $src(@sources) { # отсекаем компании с небольшой "текучестью клиентов" next if(max($plus{$src},$minus{$src}) < $max_cnt*MIN_PERCENT/100); # берем очередной цвет my $color = $colors[$i++]; my $delta = $plus{$src} - $minus{$src}; # описание вершины графа, соответствующей компании print "\"$src\" [label=\"$src\\n+".int($plus{$src}).", -". int($minus{$src}). ", delta: $delta\",style=\"filled\",fontcolor=\"#111111\",". "color=\"#$color\",". # для тех, кому больше нравятся прямоугольники :) # "shape=\"box\",width=4,height=1,". "fontsize=16];\n"; for my $to(keys %{$data{$src}}) { my $cnt = $data{$src}{$to}; my $percents = $cnt*100/$max_cnt; # слишком "тонкие" дуги не выводим if($percents > MIN_PERCENT) { # выводим описание дуги print "\"$src\" -> \"$to\" [label=\"$cnt\",fontcolor=". "\"#$color\",color=\"#$color\",penwidth=". int( 1 + $percents*12/100 ). ",fontsize=". int(12 + $percents*12/100)."];\n"; } } } # конец описания графа print "\n}\n";

Пример gv-файла, созданного с помощью этого скрипта, можно посмотреть здесь. Понятно, что все данные в нем не настоящие. Преобразуется GV в SVG следующим образом:

circo graph-fake.gv -Tsvg -o fake.svg

Здесь можно скачать полученный граф. А кому лень качать (или вдруг у кого-нибудь SVG не открывается), вот картинка поменьше:

Граф, построенный в Graphviz

Большему числу ушедших клиентов соответствуют более толстые линии. Кроме того, каждой компании соответствует свой цвет. Можно было бы еще менять размер вершин в зависимости от того, сколько всего новых клиентов получила компания, но у меня что-то не получилось сделать это красиво. Может, у вас лучше получится.

Дополнение: Вас также могут заинтересовать заметки Построение UML-диаграмм с помощью PlantUML и Рендерим математические формулы с помощью AsciiMath.