Введение в GraphViz

Однажды, собираясь писать документацию - я задумался. Проект, который я собирался документировать, динамически развивался и было очевидно, что все схемы, которые я буду рисовать ещё не раз придётся дополнять и перерисовывать. "Вот бы было что-то типа Wiki, но с возможностью также легко рисовать схемы" - подумал тогда я. "Но неужели до меня никто не додумался до столь простой мысли и не сделал столь полезного изобретения?" - меня посетили сомнения. "Не может быть!" Я отправился в поисковые системы, где во множестве предлагались различные платные графические редакторы, позволяющие рисовать прямо в браузере. Но тут мой взгляд зацепился за знакомое название GraphViz! Вспомнив, что я уже не раз слышал об этом продукте, а также не раз видел его в составе моего дистрибутива, я решил познакомится поближе и неожиданно увлёкся...

Автор статьи: Виктор Вислобоков
Размещается под лицензией: CC-BY-NC-ND

Установка


Установка до безобразия проста. GraphViz входит в состав практически всех извесных мне дистрибутивов, ориентированных на пользователя. Так что всё что остаётся, например, людям использующим Fedora, CentOS и прочие rpm-дистрибутивы с yum, это выполнить команду:

yum install graphviz

Также в дистрибутивах есть пакеты, позволяющие использовать GraphViz со многими языками прогаммирования: Perl, Python, PHP, Tcl/TK и и.д. Возможно после ознакомления с GraphViz вы захотите поставить и использовать также и эти пакеты.

Первое использование


Использовать GraphViz очень просто. Как всегда всё в стиле Unix-way, т.е. через командную строку. Вы делаете файл, в котором, на некоем мета-языке содержится описание требуемого вам графа, а затем запускаете утилиту из комплекта GraphViz, которая из этого файла делает другой файл, хранящий в себе уже сам отрисованный граф.

Простейший пример. Сделаем файл example1.gv вида:

digraph HelloWorld {
  "Hello" -> "World";
}
и получим из него картинку в формате PNG:
dot -Tpng example1.gv -oexample1.png

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

Разумеется, отрисованный граф можно получать не только в виде PNG-картинки. Это может быть и SVG-графика, встраиваемая в веб-страницу, и PDF, и PostScript, VML и т.д. и т.д.

Разумеется, GraphViz может много больше, чем просто отрисовка столь примитивного графа, который был продемонстрирован в предыдущем разделе. Возможностей очень много и их полное описание выходит за пределы данной статьи. Если вы очень любите изучать всё с начала и до конца, то я рекомендую посетить официацльный сайт на предмет чтения официальной документации: http://www.graphviz.org. Я же рассмотрю лишь некоторые полезные возможности, которые помогут вам начать рисовать достаточно продвинутые графы и схемы.

Немного теории


Хотя с помощью GraphViz можно рисовать схемы, которые лишь отдалённо напоминают графы, всё же GraphViz - это именно средство для отрисовки графов! Поэтому не ждите от неё возможностей Visio или чего-то подобного, у этого продукта (как впрочем и любого другого) есть свои ограничения и недостатки, о чём будет сказано ниже. И вот вам кстати ссылка на Галерею графов, где вы можете посмотреть, что можно получить с помощью GraphViz.

Прежде чем пойти дальше, я введу некоторые термины, которыми мы будем оперировать. Они пригодятся ниже, когда я буду давать пояснения, а также если вы вдруг всё же начнёте изучать полную документацию. Итак:

  • node - Узел графа. Обычно представляет из себя какую-либо фигуру из библиотеки встроенных фигур.
  • edge - Переводится как "край", но в GraphViz имеет совсем другое значение. Обычно это стрелка, соединяющая node (узлы графа)
  • subgraph - Подграф, который включается в основной граф. Самостоятельная область графа, которая может включать в себя другие узлы и которая затем самостоятельно отрисовывается в графе
  • cluster - Или "кластер". Это разновидность подграфа, которая может иметь метку и выделенную область, выделяемую цветом.
  • атрибут - Именованный параметр, который может изменять оформление или поведение графа, подграфа, node или edge. Атрибутов великое множество и подробное описание к ним можно найти на официальном сайте, ссылку на который я уже приводил.

Имена узлов


GraphViz по умолчанию заточен на использование UTF-8 и вы можете преспокойно использовать русские буквы в названии узлов графа. Единственное, что необходимо помнить, что лучше заключайте любые имена в двойные кавычки, хотя GraphViz и позволяет использовать имена из одного слова, не содержащие разделительных знаков без кавычек. Т.е. вы можете использовать следующие имена узлов:

node1
Понедельник
"Новый год"
"self-made"
Обратите внимание, в двух последних случаях использование двойных кавычек обязательно, потому что в имени "Новый год" два слова, разделённые через пробел, а имя "self-made" содержит служебный символ "-". В общем мой совет - используйте кавычки всегда и не ошибётесь, а напротив избежите досадных ошибок, связанных с невнимательностью.

В любом месте файла вы можете использовать комментарии также как в языке C++, т.е. либо вот так:

/* это комментарий
в несколько
строк
 */
либо вот так:
// это комментарий до конца строки

Умолчания


Если вы используете набор однотипных фигур и стрелок, но не тот, который установлен по умолчанию, гораздо удобней будет выставить вначале необходимые умолчания, которые будут действовать для всего графа. Например умолчание:

node [shape="box"]
edge [dir="both"]
приведёт к тому, что вместо эллипсов, внешний вид узлов примет вид прямоугольников, а все стрелки будут иметь два кончика, указывающие в обоих направлениях.

Примеры

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

Пример 1. Предположим вы хотите составить себе план действий на неделю, потому что периодически, что-либо вылетает из головы. Например, во вторник и субботу вы моете свой автомобиль, а по понедельникам, четвергам и субботам у вас тренировка в спортзале. А в среду, пятницу и понедельник вам нужно зайти в магазин за продуктами. А в четверг вам ещё нужно побывать в паспортном столе, чтобы забрать документы, а если они ещё будут не готовы, то подождать их сидя в кафе неподалёку. Теперь, сразу решаем, что внешний вид узлов графа должен быть круги вместо эллипсов, которые залиты синим цветом, а надписи в них будут белые. Поскольку документы - это наиболее важное в нашем плане, то мы используем красный цвет. А стрелки мы хотим, чтобы были не сплошные, а пунктирные. Да и граф нужно ориентировать не по вертикали, а по горизонтали, потому что так нам наглядней. В итоге получаем:

digraph MyPlan {
  node [shape="circle", style="filled", fillcolor="blue", fontcolor="#FFFFFF", margin="0.01"];
  edge [style="dashed"];
  rankdir="LR";
  
  "Документы" [fillcolor="red"];
  
  "Понедельник"->"Тренировка"->"Продукты";
  "Вторник"->"Мойка авто";
  "Среда"->"Продукты";
  "Четверг"->"Документы"->"Тренировка";
  "Документы"->"Кафе"->"Документы";
  "Пятница"->"Продукты";
  "Суббота"->"Мойка авто"->"Тренировка";
  "Воскресенье";
}
И картинку:

Уместно дать некоторые пояснения. Как видите, цвет можно задавать не только его названием, но и 16-ричным кодом, как в HTML (#RRGGBB). Единожды появившееся описание узла, используется повсеместно, не вызывая конфликтов. Несвязанные узлы, например "Воскресенье" так и остаются в графе без стрелочек. Атрибут margin="0.01" используется, чтобы уменьшить отступ внутри кругов, иначе они будут больше, что не всегда удобно. Атрибут rankdir="LR" меняет направление графа с "TB" (сверху вниз), на "LR" (слева направо).

Посмотрев на граф, вы видите, что он получился не таким уж и удобным. Дни недели разбросаны по всему графу, ориентироваться в нём неудобно. Может быть попробовать всё же изобразить его вертикально?

Пример 2. Давайте попробуем изобразить ранее изученный пример вертикально. Ну а чтобы было интересней, заключим дни недели в кластер, выделим его цветом и добавим метку. А вместо кругов в качестве фигур для дней недели будем использовать прямоугольники, залитые зелёным цветом. А шрифт у названий дней недели сделаем чёрного цвета и помельче. Заодно ещё и добавим в воскресенье сперва поход в театр, а затем в кафе:

digraph MyPlan {
  node [shape="circle", style="filled", fillcolor="blue", fontcolor="#FFFFFF", margin="0.01"];
  edge [style="dashed"];

  "Документы" [fillcolor="red"];

  subgraph cluster_week {
    node [shape="box", style="filled", fillcolor="green", fontcolor="black", fontsize="9"];
    label = "Дни недели";
    "Понедельник";
    "Вторник";
    "Среда";
    "Четверг";
    "Пятница";
    "Суббота";
    "Воскресенье";
  }
  
  "Понедельник"->"Тренировка"->"Продукты";
  "Вторник"->"Мойка авто";
  "Среда"->"Продукты";
  "Четверг"->"Документы"->"Тренировка";
  "Документы"->"Кафе"->"Документы";
  "Пятница"->"Продукты";
  "Суббота"->"Мойка авто"->"Тренировка";
  "Воскресенье"->"Театр"->"Кафе";
}
и картика:

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

Ещё обратите внимание, чтобы была возможность сделать рамочку и подпись к кластеру, имя у подграфа должно начинаться с "cluster" и никак иначе

.

Пример 3. Теперь давайте возьмём так сказать один день из жизни и сделаем из него блок-схему алгоритма. Пусть это будет четверг. Делаем практически то, что уже пробовали, лишь добавляем к стрелочкам, соединяющим узлы подписи "Да" и "Нет":

digraph MyPlan {
  node [shape="rectangle"];
  
  "Начало" [shape="ellipse"]
  "Конец" [shape="ellipse"]
  "Если готовы документы" [shape="diamond"]
  
  "Начало"->"Четверг"->"Если готовы документы"->"Тренировка"[label="Да"]
  "Тренировка"->"Конец";
  "Если готовы документы"->"Кафе"[label="Нет"]
  "Кафе"->"Если готовы документы";
}
получаем картинку:

И... разочаровано на неё смотрим! Коряво получилось. "Кафе" должно быть сбоку от условия "Если готовы документы", а у нас оно внизу, а стрелка к "Кафе" и обратно к "Если готовы документы" идёт совсем не по стандарту. Если изменить уровнь у "Кафе" ещё можно, использовав распределение узлов по уровням, то со стрелками уже ничего поделать нельзя. Вот вам и первый недостаток GraphViz - он не ориентирован на создание прямолинейных блок-схем алгоритмов.

Пример 4. Давайте теперь ознакомимся с возможностью GraphViz распределять узлы графа по определённым уровням высоты. Реализована эта возможность, через создание в качестве некой "линейки уровней" ещё одного невидимого графа в стороне от основного и закрепление узлов нашего графа на том же уровне, что узлов линейки. Итак, снова берём наш план и меняем его вот так:

digraph MyPlan {
  node [shape="circle", style="filled", fillcolor="blue", fontcolor="#FFFFFF", margin="0.01"];
  edge [style="dashed"];

  {
    node [shape="plaintext",style="invisible"];
    edge [color="white"];
    "1" -> "2" -> "3" -> "4" -> "5" -> "6" -> "7";
  }

  subgraph week {
    node [shape="box", style="filled", fillcolor="green", fontcolor="black", fontsize="9"];
    label = "Дни недели";
    "Понедельник";
    "Вторник";
    "Среда";
    "Четверг";
    "Пятница";
    "Суббота";
    "Воскресенье";
  { rank="same"; "1"; "Понедельник"; }
  { rank="same"; "2"; "Вторник"; }
  { rank="same"; "3"; "Среда";  }
  { rank="same"; "4"; "Четверг"; }
  { rank="same"; "5"; "Пятница"; }
  { rank="same"; "6"; "Суббота"; }
  { rank="same"; "7"; "Воскресенье"; }
  }

  "Документы" [fillcolor="red"];

  "Понедельник"->"Тренировка"->"Продукты";
  "Вторник"->"Мойка авто";
  "Среда"->"Продукты";
  "Четверг"->"Документы"->"Тренировка";
  "Документы"->"Кафе"->"Документы";
  "Пятница"->"Продукты";
  "Суббота"->"Мойка авто"->"Тренировка";
  "Воскресенье";
}
В итоге получим вот такую смешную картинку:

Обратите внимание, у узлов линейнки, установлен атрибут:

style="invisible";
который запрещает отрисовывать эти узлы, хотя и OpenViz резервирует место под них, а атрибут
color="white"
у стрелок, приводит к тому, что они отрисовываются, но белым цветом, таким же как цвет фона, поэтому их не видно!

Как вы можете видеть, теперь дни недели упорядочены по высоте, но к сожалению не находятся друг под другом. И увы, мне неизвесно способа как их можно заставить выстроится именно так! Конструкция:

  { rank="same"; "2"; "Вторник"; }
говорит, что узлы "2" и "Вторник" должны находится на одном уровне. Вы можете, например, добавить ещё какой-либо узел после "Вторник" также через ";" и этот узел также будет отрисован на этом же уровне.

Ещё один недостаток такого распределения по уровням становится ясен, когда начинаете работать с кластерами. Я пытался создать в кластере ещё один кластер и несколько узлов, распределив их по высоте, но сколь я не бился - у меня ничего не вышло. По неизвестной мне причине, уровни начинали вставать попарно друг с другом рядом. Если кто знает почему так и как это решить - напишите мне и моё "большое спасибо" вам обеспечено. Заодно дополню статью.

Пример 5. А что если хочется странного, например, чтобы название узла было в две строки? Это как раз довольно просто. А если вдруг хочется чтобы вторая строка имела другой цвет и размер шрифта? Вот это уже гораздо сложнее! А бывает вообще хочется некий прямоугольник, разбитый по секциям с разными надписями. Вот вам пример, реализующий всё вышесказанное:

digraph MyPlan {
   node [margin="0.01"];

   "box1" [shape="box", label="Надпись в\nдве строки"];
   "box2" [shape="box", style="filled", fillcolor="lightcyan2", label=<Обычная надпись<BR /><FONT COLOR="blue" POINT-SIZE="8">вторая строка другим шрифтом и цветом</FONT>>];
   "box3" [shape="record", label = "Слева|Справа"];
   "box4" [shape="record", label = "{Сверху|Снизу}"];
   "box5" [shape="record", label = "Слева|{Сверху|Центр|Снизу}|Справа"];

   "box1" -> "box2";
   "box3" -> "box4" -> box5;
}
И картинка:

Как видите, если "box1" и "box2" используют тип узла "box" и HTML-форматирование, то "box3", "box4" и "box5" используют специальный тип узла: "record". К сожалению у "record" есть недостаток - расположение меток зависит от направленности графа, т.е. если бы у этого графа атрибут "rankdir" был выставлен в "LR" а не как по умолчанию в "TB", то метки "слева" и "справа" стали бы отрисоваться не слева и справа, а сверху и снизу и наоборот метки "снизу" и "сверху" стали бы отрисовываться вместо снизу и свеху, справа и слева. Это не очень-то удобно, если вы по каким-то причинам решить поменять направление роста графа.

Пример 6. Тем не менее, GraphViz неплохо подходит для отображения структуры баз данных и связей между таблицами. Приведём пример, заодно пользуясь случаем показать, что можно изменить внешний вид наконечников стрелок:

digraph MyPlan {
   node [margin="0.01"];
   rankdir="LR";

   "users_tbl" [shape="record", label="пользователи| user_id|username|password"];
   "perms_tbl" [shape="record", label="права доступа| perm_id|permission|description"];
   "access_tbl" [shape="record", label="доступ| user_id| perm_id"];
   
   "users_tbl":PK -> "access_tbl":FK1[dir="both",arrowtail="invdot",arrowhead="odot"];
   "perms_tbl":PK -> "access_tbl":FK2[dir="both",arrowtail="invdot",arrowhead="odot"];
}
и картинка:

Заключение


В общем-то пожалуй и всё, что я хотел рассказать. Думаю, для решения множества задач - этих примеров достаточно.

Хочется заметить, что во всех примерах я использовал направленный граф и утилиту dot, однако GraphViz позволяет использовать и другие виды отрисовки графов, такие как "circo", "neato" и т.д. Так вот, чтобы попробовать отобразить свой граф в таком режиме, нужно вместо dot использовать соответствующую утилиту: circo, neato и т.д. Не все атрибуты, которые поддерживаются dot, поддерживаются другими способами отображения - нужно иметь это в виду.

ВложениеРазмер
example1.png6.59 kb
example2.png58.5 kb
example3.png178.01 kb
example4.png24.35 kb
example5.png96.85 kb
example6.png16.59 kb
example7.png13.96 kb