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

11 октября 2010

Есть в юниксах такая полезная утилита под названием 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 A 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.

Метки: , , .


Вы можете прислать свой комментарий мне на почту, или воспользоваться комментариями в Telegram-группе.