• Skip to content
Malowanie grafu
Notatnik zapisywany wieczorami
  • Start
  • Archiwum
  • Kategorie
  • Cykle
  • O notatniku
  • Kontakt

Malowanie grafu

19 marzec 2011 | Linux - używanie | View Comments

Lubię graficzne schematy. Parę kresek i kilka słów potrafi lepiej przedstawić jakąś informację niż wiele stron tekstu. Zarazem mam z nimi problem: o ile na kartce czy tablicy rysuję dość chętnie i łatwo (choć niezbyt pięknie), o tyle na komputerze idzie mi to opornie. Czy używam flowchartera, czy programu do grafiki wektorowej, czy narzędzia do UML - zamiast koncentrować się na treści, grzęznę w szczegółach. Walczę o właściwe rozmieszczenie strzałek i boksów, pracowicie przestawiam czcionki w kolejnych pudełkach, poszukuję obejść pozwalających na umieszczenia jakiejś adnotacji w odpowiednim miejscu... Chyba tylko mind-mapy są mniej pracochłonne.

Jakiś czas temu, w nieco zgryźliwym nastroju, postanowiłem puścić po firmie następujący obrazek.

Procedura obsługi zgłoszenia

Aby puścić, trzeba było zrobić. Spróbowałem graficznie (akurat w Inkscape ale w Dia sprawa jest równie beznadziejna). Węzły dały się namalować nawet dość szybko ale po paru próbach poukładania połączeń po prostu się poddałem. Czego by nie robić i jak by nie wyginać tych krzywych, albo zaczepiały o węzły, albo splatały się w nieczytelny kłąb (nie mówiąc o konieczności ciągłego przepinania).

A potem przypomniałem sobie o GraphVizie (szczęśliwie nieco wcześniej oglądanym w kontekście innego zastosowania) i w kilkanaście minut napisałem, co następuje:

digraph obsluga_zgloszenia {

// Wspólne cechy wszystkiego

size = "8.6,8.6";
   concentrate = true;
   node [shape=ellipse, fontsize=12, height=0.6, width=0.6, fixedsize=false];
   edge [style=solid];

// Stany

start [ label="Czy bug jest przypisany do mnie?" ];
   alok  [ label="Czy jestem formalnie zaalokowany?" ];
   zapyt [ label="Czy mogę się kogoś o coś spytać?" ];
   probl [ label="Czy mam jakąkolwiek trudnosć,\nnp. źle ustawiona ścieżka czy chwilowy brak sieci" ];
   pretn [ label="Czy mam pretekst by mieć do kogoś pretensje?" ];
   flamestart [ label="Czy da się zacząć jakiś flamewar?" ];
   uzup [ label="Czy upłynęło już dość czasu\nby spytać czy problem nadal występuje?" ];
   przek [ label="Przekieruj i zapomnij" ];
   flame [ label="Urraaaaa" ];
   sen [ label="Ignoruj" ];
   wait [ label="Poczekaj troszkę" ];

// Krawędzie

edge [ label="Tak" ];

start -> alok;
   alok -> zapyt;
   zapyt -> przek;
   probl -> przek;
   pretn -> przek;
   flamestart -> flame;
   uzup -> przek;

edge [ label="Nie" ];

start -> sen;
   zapyt -> probl;
   probl -> pretn;
   pretn -> flamestart;
   alok -> sen;
   flamestart -> uzup;
   uzup -> wait;

edge [ label="Tak, Nie, Nieważne" ];

flame -> flame;

edge [label = ""];

wait -> start;

}
Potem
$ dot -Tpng algorytm.dot > algorytm.png

i już.

Kilka słów o języku DOT

Mój powyższy kod nie jest przesadnie ładny (zastanawiałem się, czy go na potrzeby artykułu nie wygładzić ale ostatecznie zostawiłem autentyczny przykład). Komplikuje go zwłaszcza wprowadzenie symbolicznych etykiet węzłów, które oszczędziły mi pisania przy przestawianiu kolejności i dorzucaniu dodatkowych kroków. Czytając go trzeba też pamiętać, że w języku DOT atrybuty ustawiane przy node albo edge są globalne i obowiązują do czasu zmiany (dlatego np. wszystkie krawędzie poniżej edge [label="Tak"] są opisane słowem Tak).

Tak naprawdę w języku DOT chodzi jednak po prostu o zapisy połączeń (wszystkie te coś -> coś), cała reszta to uzupełnienia. Zupełnie sensowny diagram można zrobić pisząc po prostu:

digraph krolowie {
    "Władysław Jagiełło" -> "Władysław Warneńczyk" [label="następca"];
    "Władysław Jagiełło" -> "Kazimierz";
    "Władysław Jagiełło" -> "Kazimierz Jagiellończyk";
    "Kazimierz Jagiellończyk" -> "Jan Olbracht";
    "Kazimierz Jagiellończyk" -> "Zygmunt Stary";
}

Czy wszystko da się tak namalować? Nie, choć bynajmniej nie tylko proste grafy. Trudniejsze przykłady wymagają jednak poświęcenia chwili czasu na lekturę dokumentacji (radzę zacząć od dotguide) na co nie każdy musi mieć ochotę. Przynajmniej jednak dla prostych schematów trudno mi sobie wyobrazić szybszą metodę niż ta - wymagająca de facto jedynie wypisania listy węzłów i połączeń.

Już używasz GraphViza

Ze względu na łatwość generowania plików .dot, wiele możliwych formatów wynikowych, a także dostępność różnorakich pomocniczych bibliotek GraphViz jest używany w wielu webowych i desktopowych narzędziach. Przykładowo, właśnie dzięki niemu można oglądać hierarchie zgłoszeń w Bugzilli albo hierarchie klas w dokumentacji generowanej Doxygenem.

Emacs

Moje środowisko edycyjne do Graphviza wygląda następująco:

Graphviz w emacsie

Zapewnia mi je graphviz-dot-mode.

Problem layoutu

GraphViz oszczędza mi klikania ale przede wszystkim rozwiązuje niebanalny problem, jakim jest automatyczne rozmieszczenie elementów grafu (tak by minimalizować przecięcia, uniknąć nachodzenia węzłów na siebie i rozmieścić całość w miarę równomiernie). To jest całkiem trudne (co przy okazji uzasadnia, czemu dla niebanalnych schematów niełatwo zrobić to ręcznie).

Fajną zabawą programistyczną jest próba zrobienia tego samemu (ja próbowałem, poniosłem klęskę ale doświadczenie uważam za pouczające). Napisz skrypt, który wczyta z wejścia listę stanów i połączeń i namaluje jak najładniejszy obrazek. Wejście może wyglądać np. tak:

A,C,E,F,H,I,J,L,M,N,O,P,R,T,V,Z
A-C
A-E
A-R
A-T
C-F
E-F
E-H
E-I
E-V
F-J
H-Z
I-O
I-Z
J-P
L-J
L-N
M-J
M-P
M-Z
N-P
R-F
R-J
R-L
T-F
T-M
V-Z
Z-O
Z-P

a w samym malowaniu obrazka może być przydatny np. poniższy skrawek kodu:

# -*- coding: utf-8 -*-
#
#    Generacja pliku SVG z pomocą svgfig 
#         http://code.google.com/p/svgfig/

from svgfig import *

def make_point(x, y, txt):
    circle = SVG("circle", cx=x, cy=y, r=30, fill="white")
    txt = Text(x, y, txt, fill="black")
    txt.attr["font-size"] = 16
    txt.attr["text-anchor"] = "middle"
    txt.attr["dominant-baseline"] = "middle"
    return Fig(circle, txt)

line = Line(50, 50, 120, 170, fill="black")
point1 = make_point(50, 50," A")
point2 = make_point(120, 170, "B")

picture = canvas_outline(
    Fig(line, point1, point2).SVG(),
    width = "640px", height="640px", viewBox="1 1 640 640",
    )
picture.attr["style"] = "stroke: black; fill: white; stroke-width: 1pt; stroke-linejoin:round; text-anchor: middle; font-weight:bold"

#picture.save("test.svg")
picture.firefox("test.svg")    # Zapis do test.svg i podgląd w firefoksie

Najbardziej znany klasyczny algorytm jest opisany tutaj.

W praktyce warto jednak stosować gotowce. GraphViz nie jest jedynym, kilka algorytmów rozmieszczania grafu udostępnia boost.graph (patrz rozdział Layout Algorithms), kilkanaście OGDF, wspomnę jeszcze CCVisu oraz Microsoftowy MSAGL.

Na podobny temat:
Problemy grafiki po upgrade do Ubuntu 11.04 - wreszcie z rozwiązaniem
Myśleć obrazkami
Wyszedł GIMP 2.6 - opinia szarego człowieka
Tworzenie screencastów pod Linuksem - część 4

« Autorytet | Czarna lista shorewalla »
komentarze obsługiwane przez Disqus
  • © 2008-2012, Marcin Kasperski, All rights reserved.