├── .formatter.exs
├── .github
└── workflows
│ └── ci.yml
├── .gitignore
├── .tool-versions
├── .travis.yml
├── LICENSE.md
├── README.md
├── config
├── config.exs
├── dev.exs
└── test.exs
├── examples
├── basic.dot
├── basic.ex
├── basic.png
├── html_records.dot
├── html_records.ex
├── html_records.png
├── shapes.dot
├── shapes.ex
├── shapes.png
├── structs.dot
├── structs.ex
└── structs.png
├── lib
├── graphvix.ex
└── graphvix
│ ├── dot_helpers.ex
│ ├── graph.ex
│ ├── html_record.ex
│ ├── record.ex
│ ├── record
│ └── port.ex
│ ├── record_subset.ex
│ └── subgraph.ex
├── mix.exs
├── mix.lock
└── test
├── graphvix
├── dot_helpers_test.exs
├── graph_test.exs
├── html_record_test.exs
├── record_test.exs
└── subgraph_test.exs
└── test_helper.exs
/.formatter.exs:
--------------------------------------------------------------------------------
1 | # Used by "mix format"
2 | [
3 | inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
4 | ]
5 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: "CI"
2 | on:
3 | push:
4 | branches:
5 | - main
6 | pull_request:
7 | branches:
8 | - main
9 | workflow_dispatch:
10 |
11 | jobs:
12 | test:
13 | runs-on: ubuntu-20.04
14 | env:
15 | MIX_ENV: test
16 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
17 | name: "[${{matrix.otp}}/${{matrix.elixir}}] CI Tests"
18 | strategy:
19 | matrix:
20 | otp: [22, 23, 24, 25]
21 | elixir: ["1.13.4", "1.14.0"]
22 | exclude:
23 | - otp: 22
24 | elixir: "1.14.0"
25 | steps:
26 | - uses: actions/checkout@v2.4.0
27 |
28 | - uses: erlef/setup-beam@v1
29 | with:
30 | otp-version: ${{matrix.otp}}
31 | elixir-version: ${{matrix.elixir}}
32 |
33 | - name: mix-cache
34 | uses: actions/cache@v2
35 | id: mix-cache
36 | with:
37 | path: |
38 | deps
39 | _build
40 | key: ${{ runner.os }}-${{ matrix.otp }}-${{ matrix.elixir }}-${{ hashFiles('mix.lock') }}
41 |
42 | - name: mix local
43 | run: |
44 | mix local.rebar --force
45 | mix local.hex --force
46 |
47 | - name: mix compile
48 | run: |
49 | mix deps.get
50 | mix deps.compile
51 | mix compile
52 | if: steps.mix-cache.outputs.cache-hit != 'true'
53 |
54 | - name: mix checks
55 | run: |
56 | mix deps.unlock --check-unused
57 | mix format --check-formatted
58 |
59 | - name: mix test
60 | run: mix test
61 |
62 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # The directory Mix will write compiled artifacts to.
2 | /_build
3 |
4 | # If you run "mix test --cover", coverage assets end up here.
5 | /cover
6 |
7 | # The directory Mix downloads your dependencies sources to.
8 | /deps
9 |
10 | # Where 3rd-party dependencies like ExDoc output generated docs.
11 | /doc
12 |
13 | # If the VM crashes, it generates a dump, let's ignore it too.
14 | erl_crash.dump
15 |
16 | # Also ignore archive artifacts (built via "mix archive.build").
17 | *.ez
18 |
19 | graphvix.store
20 | *.dot
21 | *.pdf
22 | *.png
23 |
--------------------------------------------------------------------------------
/.tool-versions:
--------------------------------------------------------------------------------
1 | elixir 1.14.0
2 | erlang 25.0.4
3 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: elixir
2 | elixir:
3 | - 1.5
4 | - 1.6
5 | - 1.7
6 | otp_release:
7 | - 19.0
8 | - 20.3
9 | matrix:
10 | include:
11 | - elixir: 1.8
12 | otp_release: 20.3
13 | - elixir: 1.8
14 | otp_release: 21.0
15 | script: mix test
16 | env:
17 | - MIX_ENV=test
18 | before_install:
19 | - sudo apt-get install graphviz
20 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | **Copyright (c) 2016 Michael Berkowitz**
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy
4 | of this software and associated documentation files (the "Software"), to deal
5 | in the Software without restriction, including without limitation the rights
6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
7 | of the Software, and to permit persons to whom the Software is furnished to do
8 | so, subject to the following conditions:
9 |
10 | The above copyright notice and this permission notice shall be included in all
11 | copies or substantial portions of the Software.
12 |
13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
15 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
16 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
17 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
18 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
19 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Graphvix
2 |
3 | [](https://travis-ci.org/mikowitz/graphvix)
4 |
5 | Graphviz in Elixir
6 |
7 | ## Installation
8 |
9 | If [available in Hex](https://hex.pm/docs/publish), the package can be installed as:
10 |
11 | 1. Add `graphvix` to your list of dependencies in `mix.exs`:
12 |
13 | ```elixir
14 | def deps do
15 | [{:graphvix, "~> 1.0.0"}]
16 | end
17 | ```
18 |
19 | # Usage
20 |
21 | See [the wiki](https://github.com/mikowitz/graphvix/wiki/Examples) for examples.
22 |
23 | ## API Overview
24 |
25 | * Create a new graph
26 |
27 | `Graphvix.Graph.new/0`
28 |
29 | * Add a vertex to a graph
30 |
31 | `Graphvix.Graph.add_vertex/2`
32 | `Graphvix.Graph.add_vertex/3`
33 |
34 | * Add an edge between two vertices
35 |
36 | `Graphvix.Graph.add_edge/3`
37 | `Graphvix.Graph.add_edge/4`
38 |
39 | * Create a vertex with type `record`
40 |
41 | `Graphvix.Record.new/1`
42 | `Graphvix.Record.new/2`
43 |
44 | * Add a record vertex to a graph
45 |
46 | `Graphvix.Graph.add_record/2`
47 |
48 | * Create a vertex using HTML table markup
49 |
50 | `Graphvix.HTMLRecord.new/1`
51 | `Graphvix.HTMLRecord.new/2`
52 |
53 | * Add an HTML table vertex to a graph
54 |
55 | `Graphvix.Graph.add_html_record/2`
56 |
57 | * Save a graph to disk in `.dot` format
58 |
59 | `Graphvix.Graph.write/2`
60 |
61 | * Save and compile a graph (defaults to `.png`)
62 |
63 | `Graphvix.Graph.compile/2`
64 | `Graphvix.Graph.compile/3`
65 |
66 | * Save, compile and open a graph (defaults to `.png` and your OS's default image viewer)
67 |
68 | `Graphvix.Graph.graph/2`
69 | `Graphvix.Graph.graph/3`
70 |
71 | ## Basic Usage
72 |
73 | 1. Alias the necessary module for ease of use
74 |
75 | ```elixir
76 | alias Graphvix.Graph
77 | ```
78 |
79 | 1. Create a new graph.
80 |
81 | ```elixir
82 | graph = Graph.new()
83 | ```
84 |
85 | 1. Add a simple vertex with a label
86 |
87 | ```elixir
88 | {graph, vertex_id} = Graph.add_vertex(graph, "vertex label")
89 | ```
90 |
91 | 1. Add a vertex with a label and attributes
92 |
93 | ```elixir
94 | {graph, vertex2_id} = Graph.add_vertex(
95 | graph,
96 | "my other vertex",
97 | color: "blue", shape: "diamond"
98 | )
99 | ```
100 |
101 | 1. Add an edge between two existing vertices
102 |
103 | ```elixir
104 | {graph, edge_id} = Graph.add_edge(
105 | graph,
106 | vertex_id, vertex2_id,
107 | label: "Edge", color: "green"
108 | )
109 | ```
110 |
111 | 1. Add a cluster containing one or more nodes
112 |
113 | ```elixir
114 | {graph, cluster_id} = Graph.add_cluster(graph, [vertex_id, vertex2_id])
115 | ```
116 |
117 | ## Records
118 |
119 | 1. Alias the necessary module for ease of use
120 |
121 | ```elixir
122 | alias Graphvix.Record
123 | ```
124 |
125 | 1. Create a simple record that contains only a row of cells
126 |
127 | ```elixir
128 | record = Record.new(Record.row(["a", "b", "c"]))
129 | ```
130 |
131 | * A record with a top-level row can also be created by just passing a list
132 |
133 | ```elixir
134 | record = Record.new(["a", "b", "c"])
135 | ```
136 | 1. Create a record with a single column of cells
137 |
138 | ```elixir
139 | record = Record.new(Record.column(["a", "b", "c"]))
140 | ```
141 |
142 | 1. Create a record with nested rows and columns
143 |
144 | ```elixir
145 | import Graphvix.Record, only: [column: 1, row: 1]
146 |
147 | record = Record.new(row([
148 | "a",
149 | column([
150 | "b",
151 | row(["c", "d", "e"]),
152 | "f"
153 | ]),
154 | "g"
155 | ])
156 | ```
157 |
158 | ### Ports
159 |
160 | 1. Ports can be attached to record cells by passing a tuple of `{port_name, label}`
161 |
162 | ```elixir
163 | import Graphvix.Record, only: [column: 1, row: 1]
164 |
165 | record = Record.new(row([
166 | {"port_a", "a"},
167 | column([
168 | "b",
169 | row(["c", {"port_d", "d"}, "e"]),
170 | "f"
171 | ]),
172 | "g"
173 | ])
174 | ```
175 |
176 | 1. Edges can be drawn from specific ports on a record
177 |
178 | ```elixir
179 | {graph, record_id} = Graph.add_record(graph, record)
180 |
181 | {graph, _edge_id} = Graph.add_edge({record_id, "port_a"}, vertex_id)
182 |
183 | ```
184 |
185 | ## HTML Table Records
186 |
187 | 1. Alias the necessary modules for ease of use
188 |
189 | ```elixir
190 | alias Graphvix.HTMLRecord
191 | ```
192 |
193 | 1. Create a simple table
194 |
195 | ```elixir
196 | record = HTMLRecord.new([
197 | tr([
198 | td("a"),
199 | td("b"),
200 | td("c")
201 | ]),
202 | tr([
203 | td("d", port: "port_d"),
204 | td("e"),
205 | td("f")
206 | ])
207 | ])
208 | ```
209 |
210 | 1. Or a more complex table
211 |
212 | ```elixir
213 | record = HTMLRecord.new([
214 | tr([
215 | td("a", rowspan: 3),
216 | td("b", colspan: 2),
217 | td("f", rowspan: 3)
218 | ]),
219 | tr([
220 | td("c"),
221 | td("d")
222 | ])
223 | tr([
224 | td("e", colspan: 2)
225 | ])
226 | ])
227 | ```
228 |
229 | Cells can also use the `font/2` and `br/0` helper methods to add font styling and forced line breaks. See
230 | the documentation for `Graphvix.HTMLRecord` for examples.
231 |
232 | ## Output
233 |
234 | 1. Convert the graph to DOT format
235 |
236 | ```elixir
237 | Graph.to_dot(graph)
238 | """
239 | digraph G {
240 | cluster c0 {
241 | v0 [label="vertex label"]
242 | v1 [label="my other vertex",color="blue",shape="diamond"]
243 |
244 | v0 -> v1 [label="Edge",color="green"]
245 | }
246 | }
247 | """
248 | ```
249 | 1. Save the graph to a .dot file, with an optional filename
250 |
251 | ```elixir
252 | Graph.write(graph, "first_graph") #=> creates "first_graph.dot"
253 | ```
254 |
255 | 1. Compile the graph to a .png or .pdf using the `dot` command
256 |
257 | ```elixir
258 | ## creates first_graph.dot and first_graph.png
259 | Graph.compile(graph, "first_graph")
260 |
261 | ## creates first_graph.dot and first_graph.pdf
262 | Graph.compile(graph, "first_graph", :pdf)
263 | ```
264 |
265 | 1. Compile the graph using the `dot` command and open the resulting file
266 |
267 | ```elixir
268 | ## creates first_graph.dot and first_graph.pdf; opens first_graph.png
269 | Graph.graph(graph, "first_graph")
270 |
271 | ## creates first_graph.dot and first_graph.pdf; opens first_graph.pdf
272 | Graph.graph(graph, "first_graph", :pdf)
273 | ```
274 |
275 |
--------------------------------------------------------------------------------
/config/config.exs:
--------------------------------------------------------------------------------
1 | import Config
2 |
3 | config :graphvix, :compiler, "dot"
4 |
5 | import_config "#{Mix.env()}.exs"
6 |
--------------------------------------------------------------------------------
/config/dev.exs:
--------------------------------------------------------------------------------
1 | import Config
2 |
--------------------------------------------------------------------------------
/config/test.exs:
--------------------------------------------------------------------------------
1 | import Config
2 |
3 | config :mix_test_watch,
4 | clear: true,
5 | tasks: [
6 | "test",
7 | "credo --all --strict",
8 | "docs"
9 | ]
10 |
11 | config :graphvix, :compiler, "echo"
12 |
--------------------------------------------------------------------------------
/examples/basic.dot:
--------------------------------------------------------------------------------
1 | digraph G {
2 |
3 | size="4,4"
4 |
5 | v0 [label="main",shape="box"]
6 | v1 [label="parse"]
7 | v2 [label="execute"]
8 | v3 [label="init"]
9 | v4 [label="cleanup"]
10 | v5 [label="make a
11 | string"]
12 | v6 [label="printf"]
13 | v7 [label="compare",shape="box",style="filled",color=".7 .3 1.0"]
14 |
15 | v0 -> v1 [weight="8"]
16 | v1 -> v2
17 | v2 -> v7 [color="red"]
18 | v0 -> v3 [style="dotted"]
19 | v0 -> v6 [style="bold",label="100 times"]
20 | v0 -> v4
21 | v2 -> v5
22 | v2 -> v6
23 | v3 -> v5
24 |
25 | }
--------------------------------------------------------------------------------
/examples/basic.ex:
--------------------------------------------------------------------------------
1 | alias Graphvix.Graph
2 |
3 | graph = Graph.new
4 |
5 | graph = Graph.set_graph_property(graph, :size, "4,4")
6 |
7 | {graph, main} = Graph.add_vertex(graph, "main", shape: "box")
8 | {graph, parse} = Graph.add_vertex(graph, "parse")
9 | {graph, execute} = Graph.add_vertex(graph, "execute")
10 | {graph, init} = Graph.add_vertex(graph, "init")
11 | {graph, cleanup} = Graph.add_vertex(graph, "cleanup")
12 | {graph, make_string} = Graph.add_vertex(graph, "make a\nstring")
13 | {graph, printf} = Graph.add_vertex(graph, "printf")
14 | {graph, compare} = Graph.add_vertex(graph, "compare", shape: "box", style: "filled", color: ".7 .3 1.0")
15 |
16 | {graph, _} = Graph.add_edge(graph, main, parse, weight: 8)
17 | {graph, _} = Graph.add_edge(graph, parse, execute)
18 | {graph, _} = Graph.add_edge(graph, execute, compare, color: "red")
19 | {graph, _} = Graph.add_edge(graph, main, init, style: "dotted")
20 | {graph, _} = Graph.add_edge(graph, main, printf, style: "bold", label: "100 times")
21 | {graph, _} = Graph.add_edge(graph, main, cleanup)
22 | {graph, _} = Graph.add_edge(graph, execute, make_string)
23 | {graph, _} = Graph.add_edge(graph, execute, printf)
24 | {graph, _} = Graph.add_edge(graph, init, make_string)
25 |
26 | Graph.write(graph, "examples/basic.dot")
27 |
--------------------------------------------------------------------------------
/examples/basic.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mikowitz/graphvix/f44aa9d5d5d2d8a19af657e5b3be55304c382c88/examples/basic.png
--------------------------------------------------------------------------------
/examples/html_records.dot:
--------------------------------------------------------------------------------
1 | digraph G {
2 |
3 | v0 [label=<
4 |
5 | left |
6 | mid dle |
7 | right |
8 |
9 |
>,shape="plaintext"]
10 | v1 [label=<
11 |
12 | one |
13 | two |
14 |
15 |
>,shape="plaintext"]
16 | v2 [label=<
17 |
18 | hello world |
19 | b |
20 | g |
21 | h |
22 |
23 |
24 | c |
25 | d |
26 | e |
27 |
28 |
29 | f |
30 |
31 |
>,shape="plaintext"]
32 |
33 | v0:f1 -> v1:f0
34 | v0:f2 -> v2:here
35 |
36 | }
--------------------------------------------------------------------------------
/examples/html_records.ex:
--------------------------------------------------------------------------------
1 | alias Graphvix.{Graph, HTMLRecord}
2 | import Graph.HTMLRecord, only: [tr: 1, td: 1, td: 2]
3 |
4 | graph = Graph.new()
5 |
6 | top_record = HTMLRecord.new([
7 | tr([
8 | td("left", port: "f0"),
9 | td("mid dle", port: "f1"),
10 | td("right", port: "f2"),
11 | ])
12 | ], border: 0, cellborder: 1, cellspacing: 0)
13 |
14 | one_two_record = HTMLRecord.new([
15 | tr([
16 | td("one", port: "f0"),
17 | td("two", port: "f1")
18 | ])
19 | ], border: 0, cellborder: 1, cellspacing: 0)
20 |
21 | hello_record = HTMLRecord.new([
22 | tr([
23 | td("hello
world", rowspan: 3),
24 | td("b", colspan: 3),
25 | td("g", rowspan: 3),
26 | td("h", rowspan: 3)
27 | ]),
28 | tr([
29 | td("c"),
30 | td("d", port: "here"),
31 | td("e")
32 | ]),
33 | tr([
34 | td("f", colspan: 3)
35 | ])
36 | ], border: 0, cellborder: 1, cellspacing: 0)
37 |
38 | {graph, top} = Graph.add_html_record(graph, top_record)
39 | {graph, one_two} = Graph.add_html_record(graph, one_two_record)
40 | {graph, hello} = Graph.add_html_record(graph, hello_record)
41 |
42 | {graph, _} = Graph.add_edge(graph, {top, "f1"}, {one_two, "f0"})
43 | {graph, _} = Graph.add_edge(graph, {top, "f2"}, {hello, "here"})
44 |
45 | Graph.write(graph, "examples/html_records.dot")
46 |
--------------------------------------------------------------------------------
/examples/html_records.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mikowitz/graphvix/f44aa9d5d5d2d8a19af657e5b3be55304c382c88/examples/html_records.png
--------------------------------------------------------------------------------
/examples/shapes.dot:
--------------------------------------------------------------------------------
1 | digraph G {
2 |
3 | size="4,4"
4 |
5 | v0 [label="a",shape="polygon",sides="5",peripheries="3",color="lightblue",style="filled"]
6 | v1 [label="b"]
7 | v2 [label="hello world",shape="polygon",sides="4",skew=".4"]
8 | v3 [label="d",shape="invtriangle"]
9 | v4 [label="e",shape="polygon",sides="4",distortion=".7"]
10 |
11 | v0 -> v1
12 | v1 -> v2
13 | v1 -> v3
14 |
15 | }
--------------------------------------------------------------------------------
/examples/shapes.ex:
--------------------------------------------------------------------------------
1 | alias Graphvix.Graph
2 |
3 | graph = Graph.new()
4 |
5 | graph = Graph.set_graph_property(graph, :size, "4,4")
6 |
7 | {graph, a} = Graph.add_vertex(graph, "a", shape: "polygon", sides: 5, peripheries: 3, color: "lightblue", style: "filled")
8 | {graph, b} = Graph.add_vertex(graph, "b")
9 | {graph, c} = Graph.add_vertex(graph, "hello world", shape: "polygon", sides: 4, skew: ".4")
10 | {graph, d} = Graph.add_vertex(graph, "d", shape: "invtriangle")
11 | {graph, e} = Graph.add_vertex(graph, "e", shape: "polygon", sides: 4, distortion: ".7")
12 |
13 | {graph, _} = Graph.add_edge(graph, a, b)
14 | {graph, _} = Graph.add_edge(graph, b, c)
15 | {graph, _} = Graph.add_edge(graph, b, d)
16 |
17 | Graph.write(graph, "examples/shapes.dot")
18 |
--------------------------------------------------------------------------------
/examples/shapes.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mikowitz/graphvix/f44aa9d5d5d2d8a19af657e5b3be55304c382c88/examples/shapes.png
--------------------------------------------------------------------------------
/examples/structs.dot:
--------------------------------------------------------------------------------
1 | digraph G {
2 |
3 | v0 [label=" left | mid\ dle | right",shape="record"]
4 | v1 [label=" one | two",shape="record"]
5 | v2 [label="hello\nworld | { b | { c | d | e } | f } | g | h",shape="record"]
6 |
7 | v0 -> v1
8 | v0 -> v2
9 |
10 | }
--------------------------------------------------------------------------------
/examples/structs.ex:
--------------------------------------------------------------------------------
1 | alias Graphvix.{Graph, Record, RecordSubset}
2 |
3 | graph = Graph.new()
4 |
5 | top_record = Record.new([{"f0", "left"}, {"f1", "mid\\ dle"}, {"f2", "right"}])
6 | one_two_record = Record.new([{"f0", "one"}, {"f1", "two"}])
7 | hello_record = Record.new([
8 | "hello\\nworld",
9 | Record.column([
10 | "b",
11 | Record.row(["c", {"here", "d"}, "e"]),
12 | "f"
13 | ]),
14 | "g",
15 | "h"
16 | ])
17 |
18 | {graph, top} = Graph.add_record(graph, top_record)
19 | {graph, one_two} = Graph.add_record(graph, one_two_record)
20 | {graph, hello} = Graph.add_record(graph, hello_record)
21 |
22 | {graph, _} = Graph.add_edge(graph, top, one_two)
23 | {graph, _} = Graph.add_edge(graph, top, hello)
24 |
25 | Graph.write(graph, "examples/structs.dot")
26 |
--------------------------------------------------------------------------------
/examples/structs.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mikowitz/graphvix/f44aa9d5d5d2d8a19af657e5b3be55304c382c88/examples/structs.png
--------------------------------------------------------------------------------
/lib/graphvix.ex:
--------------------------------------------------------------------------------
1 | defmodule Graphvix do
2 | @moduledoc """
3 | `Graphvix` provides an Elixir interface to [Graphviz](http://www.graphviz.org/)
4 | notation.
5 |
6 | With `Graphvix` you can iteratively construct directed graphs, save them to
7 | disk in `.dot` format, and print them.
8 |
9 | ### Create a new graph
10 |
11 | iex> graph = Graph.new(edge: [style: "dotted"])
12 |
13 | ### Add vertices
14 |
15 | iex> {graph, v1} = Graph.add_vertex(graph, "first vertex", color: "blue")
16 | iex> {graph, v2} = Graph.add_vertex(graph, "second vertex", color: "red")
17 | iex> {graph, v3} = Graph.add_vertex(graph, "hello", shape: "square")
18 |
19 | ### Add vertices with nested record structure
20 |
21 | iex> record = Graphvix.Record.new(row(["a", {"port_b", "b"}, col(["c", "d", "e"])]))
22 | iex> {graph, v4} = Graph.add_record(graph, record)
23 |
24 | ### Add vertices with HTML-style tables
25 |
26 | iex> html_record = Graphvix.HTMLRecord.new([tr([td("a", port: "port_a", colspan: 2)]), tr([td("b"), td(["c", br(), "d"])])])
27 | iex> {graph, v5} = Graph.add_record(graph, html_record)
28 |
29 | ### Group vertices into subgraphs and clusters
30 |
31 | iex> {graph, cluster1} = Graph.add_cluster(graph, [v1, v3, v5], style: "filled")
32 |
33 | ### Connect vertices and their ports with edges
34 |
35 | iex> {graph, e1} = Graph.add_edge(graph, v1, v2)
36 | iex> {graph, e2} = Graph.add_edge(graph, v1, {v4, "port_b"})
37 | iex> {graph, e3} = Graph.add_edge(graph, {v5, "port_a"}, v3, color: "red")
38 |
39 |
40 | ### Write the graph to a `.dot` file on your local filesystem
41 |
42 | iex> Graph.write(graph, "my_graph") #=> Creates `my_graph.dot`
43 |
44 | See the `Graphvix.Graph` documentation for more detailed usage examples.
45 | """
46 | end
47 |
--------------------------------------------------------------------------------
/lib/graphvix/dot_helpers.ex:
--------------------------------------------------------------------------------
1 | defmodule Graphvix.DotHelpers do
2 | @moduledoc """
3 | This module contains a set of helper methods for converting Elixir graph data
4 | into its DOT representation.
5 | """
6 |
7 | @doc """
8 | Convert top-level node and edge properties for a graph or subgraph into
9 | correct DOT notation.
10 |
11 | ## Example
12 |
13 | iex> graph = Graph.new(edge: [color: "green", style: "dotted"], node: [color: "blue"])
14 | iex> DotHelpers.global_properties_to_dot(graph)
15 | ~S( node [color="blue"]
16 | edge [color="green",style="dotted"])
17 |
18 | """
19 | def global_properties_to_dot(graph) do
20 | [:node, :edge]
21 | |> Enum.map(&_global_properties_to_dot(graph, &1))
22 | |> compact()
23 | |> case do
24 | [] -> nil
25 | global_props -> Enum.join(global_props, "\n")
26 | end
27 | end
28 |
29 | @doc """
30 | Converts a list of attributes into a properly formatted list of DOT attributes.
31 |
32 | ## Examples
33 |
34 | iex> DotHelpers.attributes_to_dot(color: "blue", shape: "circle")
35 | ~S([color="blue",shape="circle"])
36 |
37 | """
38 | def attributes_to_dot([]), do: nil
39 |
40 | def attributes_to_dot(attributes) do
41 | [
42 | "[",
43 | attributes
44 | |> Enum.map_join(",", fn {key, value} ->
45 | attribute_to_dot(key, value)
46 | end),
47 | "]"
48 | ]
49 | |> Enum.join("")
50 | end
51 |
52 | @doc """
53 | Convert a single atribute to DOT format for inclusion in a list of attributes.
54 |
55 | ## Examples
56 |
57 | iex> DotHelpers.attribute_to_dot(:color, "blue")
58 | ~S(color="blue")
59 |
60 | There is one special case this function handles, which is the label for a record
61 | using HTML to build a table. In this case the generated HTML label must be
62 | surrounded by a set of angle brackets `< ... >` instead of double quotes.
63 |
64 | iex> DotHelpers.attribute_to_dot(:label, "")
65 | "label=<>"
66 |
67 | """
68 | def attribute_to_dot(:label, value = " _) do
69 | ~s(label=<#{value}>)
70 | end
71 |
72 | def attribute_to_dot(key, value) do
73 | ~s(#{key}="#{value}")
74 | end
75 |
76 | @doc """
77 | Indent a single line or block of text.
78 |
79 | An optional second argument can be provided to tell the function how deep
80 | to indent (defaults to one level).
81 |
82 | ## Examples
83 |
84 | iex> DotHelpers.indent("hello")
85 | " hello"
86 |
87 | iex> DotHelpers.indent("hello", 3)
88 | " hello"
89 |
90 | iex> DotHelpers.indent("line one\\n line two\\nline three")
91 | " line one\\n line two\\n line three"
92 |
93 | """
94 | def indent(string, depth \\ 1)
95 |
96 | def indent(string, depth) when is_bitstring(string) do
97 | string
98 | |> String.split("\n")
99 | |> indent(depth)
100 | |> Enum.join("\n")
101 | end
102 |
103 | def indent(list, depth) when is_list(list) do
104 | Enum.map(list, fn s -> String.duplicate(" ", depth) <> s end)
105 | end
106 |
107 | @doc """
108 | Maps a collection of vertices or nodes to their correct DOT format.
109 |
110 |
111 | The first argument is a reference to an ETS table or the list of results
112 | from an ETS table. The second argument is the function used to format
113 | each element in the collection.
114 | """
115 | def elements_to_dot(table, formatting_func) when is_reference(table) or is_integer(table) do
116 | table |> :ets.tab2list() |> elements_to_dot(formatting_func)
117 | end
118 |
119 | def elements_to_dot(list, formatting_func) when is_list(list) do
120 | list
121 | |> sort_elements_by_id()
122 | |> Enum.map(&formatting_func.(&1))
123 | |> compact()
124 | |> return_joined_list_or_nil()
125 | end
126 |
127 | @doc """
128 | Returns nil if an empty list is passed in. Returns the elements of the list
129 | joined by the optional second parameter (defaults to `\n` otherwise.
130 |
131 | ## Examples
132 |
133 | iex> DotHelpers.return_joined_list_or_nil([])
134 | nil
135 |
136 | iex> DotHelpers.return_joined_list_or_nil([], "-")
137 | nil
138 |
139 | iex> DotHelpers.return_joined_list_or_nil(["a", "b", "c"])
140 | "a\\nb\\nc"
141 |
142 | iex> DotHelpers.return_joined_list_or_nil(["a", "b", "c"], "-")
143 | "a-b-c"
144 |
145 | """
146 | def return_joined_list_or_nil(list, joiner \\ "\n")
147 | def return_joined_list_or_nil([], _joiner), do: nil
148 |
149 | def return_joined_list_or_nil(list, joiner) do
150 | Enum.join(list, joiner)
151 | end
152 |
153 | @doc """
154 | Takes a list of elements returned from the vertex or edge table and sorts
155 | them by their ID.
156 |
157 | This ensures that vertices and edges are written into the `.dot` file in the
158 | same order they were added to the ETS tables. This is important as the order
159 | of vertices and edges in a `.dot` file can ultimately affect the final
160 | layout of the graph.
161 | """
162 | def sort_elements_by_id(elements) do
163 | Enum.sort_by(elements, fn element ->
164 | [[_ | id] | _] = Tuple.to_list(element)
165 | id
166 | end)
167 | end
168 |
169 | @doc """
170 | Removes all `nil` elements from an list.
171 |
172 | ## Examples
173 |
174 | iex> DotHelpers.compact([])
175 | []
176 |
177 | iex> DotHelpers.compact(["a", nil, "b", nil, 1])
178 | ["a", "b", 1]
179 |
180 | """
181 | def compact(enum), do: Enum.reject(enum, &is_nil/1)
182 |
183 | ## Private
184 |
185 | defp _global_properties_to_dot(%{global_properties: global_props}, key) do
186 | with props <- Keyword.get(global_props, key) do
187 | case length(props) do
188 | 0 -> nil
189 | _ -> indent("#{key} #{attributes_to_dot(props)}")
190 | end
191 | end
192 | end
193 | end
194 |
--------------------------------------------------------------------------------
/lib/graphvix/graph.ex:
--------------------------------------------------------------------------------
1 | defmodule Graphvix.Graph do
2 | @moduledoc """
3 | Models a directed graph that can be written to disk and displayed using
4 | [Graphviz](http://www.graphviz.org/) notation.
5 |
6 | Graphs are created by
7 |
8 | * adding vertices of various formats to a graph
9 | * `add_vertex/3`
10 | * `add_record/2`
11 | * `add_html_record/2`
12 | * connecting them with edges
13 | * `add_edge/4`
14 | * grouping them into subgraphs and clusters,
15 | * `add_subgraph/3`
16 | * `add_cluster/3`
17 | * providing styling to all these elements
18 | * `set_graph_property/3`
19 | * `set_global_properties/3`
20 |
21 | They can then be
22 |
23 | * written to disk in `.dot` format
24 | * `write/2`
25 | * compiled and displayed in any number of image formats (`Graphvix` defaults to `.png`)
26 | * `compile/3`
27 | * `show/2`
28 |
29 | """
30 | import Graphvix.DotHelpers
31 |
32 | alias Graphvix.{HTMLRecord, Record}
33 |
34 | defstruct digraph: nil,
35 | global_properties: [node: [], edge: []],
36 | graph_properties: [],
37 | subgraphs: []
38 |
39 | @type digraph :: {:digraph, reference(), reference(), reference(), boolean()}
40 | @type t :: %__MODULE__{
41 | digraph: digraph(),
42 | global_properties: keyword(),
43 | graph_properties: keyword(),
44 | subgraphs: list()
45 | }
46 |
47 | @compiler Application.compile_env(:graphvix, :compiler, "dot")
48 |
49 | @doc """
50 | Creates a new `Graph` struct.
51 |
52 | A `Graph` struct consists of an Erlang `digraph` record, a list of subgraphs,
53 | and two keyword lists of properties.
54 |
55 | ## Examples
56 |
57 | iex> graph = Graph.new()
58 | iex> Graph.to_dot(graph)
59 | ~S(digraph G {
60 |
61 | })
62 |
63 | iex> graph = Graph.new(graph: [size: "4x4"], node: [shape: "record"])
64 | iex> Graph.to_dot(graph)
65 | ~S(digraph G {
66 |
67 | size="4x4"
68 |
69 | node [shape="record"]
70 |
71 | })
72 |
73 | """
74 | def new(attributes \\ []) do
75 | digraph = :digraph.new()
76 | [_, _, ntab] = _digraph_tables(digraph)
77 | true = :ets.insert(ntab, {:"$sid", 0})
78 |
79 | %__MODULE__{
80 | digraph: digraph,
81 | global_properties: [
82 | node: Keyword.get(attributes, :node, []),
83 | edge: Keyword.get(attributes, :edge, [])
84 | ],
85 | graph_properties: Keyword.get(attributes, :graph, [])
86 | }
87 | end
88 |
89 | @doc """
90 |
91 | Destructures the references to the ETS tables for vertices, edges, and
92 | neighbours from the `Graph` struct.
93 |
94 | ## Examples
95 |
96 | iex> graph = Graph.new()
97 | iex> Graph.digraph_tables(graph)
98 | [
99 | #Reference<0.4011094290.3698196484.157076>,
100 | #Reference<0.4011094290.3698196484.157077>,
101 | #Reference<0.4011094290.3698196484.157078>
102 | ]
103 |
104 | """
105 | def digraph_tables(%__MODULE__{digraph: graph}), do: _digraph_tables(graph)
106 |
107 | defp _digraph_tables({:digraph, vtab, etab, ntab, _}) do
108 | [vtab, etab, ntab]
109 | end
110 |
111 | @doc """
112 | Adds a vertex to `graph`.
113 |
114 | The vertex's label text is the argument `label`, and additional attributes
115 | can be passed in as `attributes`. It returns a tuple of the updated graph
116 | and the `:digraph`-assigned ID for the new vertex.
117 |
118 | ## Examples
119 |
120 | iex> graph = Graph.new()
121 | iex> {_graph, vid} = Graph.add_vertex(graph, "hello", color: "blue")
122 | iex> vid
123 | [:"$v" | 0]
124 |
125 | """
126 | def add_vertex(graph, label, attributes \\ []) do
127 | next_id = get_and_increment_vertex_id(graph)
128 | attributes = Keyword.put(attributes, :label, label)
129 | vertex_id = [:"$v" | next_id]
130 | vid = :digraph.add_vertex(graph.digraph, vertex_id, attributes)
131 | {graph, vid}
132 | end
133 |
134 | @doc """
135 | Add an edge between two vertices in a graph.
136 |
137 | It takes 3 required arguments and one optional. The first argument is the graph,
138 | the second two arguments are the tail and head of the edge respectively, and the
139 | fourth, optional, argument is a list of layout attributes to apply to the edge.
140 |
141 | The arguments for the ends of the edge can each be either the id of a vertex, or
142 | a tuple of a vertex id and a port name to attach the edge to. This second
143 | option is only valid with `Record` or `HTMLRecord` vertices.
144 |
145 | ## Examples
146 |
147 | iex> graph = Graph.new()
148 | iex> {graph, v1id} = Graph.add_vertex(graph, "start")
149 | iex> {graph, v2id} = Graph.add_vertex(graph, "end")
150 | iex> {_graph, eid} = Graph.add_edge(graph, v1id, v2id, color: "green")
151 | iex> eid
152 | [:"$e" | 0]
153 |
154 | """
155 | def add_edge(graph, out_from, in_to, attributes \\ [])
156 |
157 | def add_edge(graph, {id = [:"$v" | _], port}, in_to, attributes) do
158 | add_edge(graph, id, in_to, Keyword.put(attributes, :outport, port))
159 | end
160 |
161 | def add_edge(graph, out_from, {id = [:"$v" | _], port}, attributes) do
162 | add_edge(graph, out_from, id, Keyword.put(attributes, :inport, port))
163 | end
164 |
165 | def add_edge(graph, out_from, in_to, attributes) do
166 | eid = :digraph.add_edge(graph.digraph, out_from, in_to, attributes)
167 | {graph, eid}
168 | end
169 |
170 | @doc """
171 | Group a set of vertices into a subgraph within a graph.
172 |
173 | In addition to the graph and the vertex ids, you can pass attributes for
174 | `node` and `edge` to apply common styling to the vertices included
175 | in the subgraph, as well as the edges between two vertices both in the subgraph.
176 |
177 | ## Examples
178 |
179 | iex> graph = Graph.new()
180 | iex> {graph, v1id} = Graph.add_vertex(graph, "start")
181 | iex> {graph, v2id} = Graph.add_vertex(graph, "end")
182 | iex> {_graph, sid} = Graph.add_subgraph(
183 | ...> graph, [v1id, v2id],
184 | ...> node: [shape: "triangle"],
185 | ...> edge: [style: "dotted"]
186 | ...> )
187 | iex> sid
188 | "subgraph0"
189 |
190 | """
191 | def add_subgraph(graph, vertex_ids, properties \\ []) do
192 | _add_subgraph(graph, vertex_ids, properties, false)
193 | end
194 |
195 | @doc """
196 | Group a set of vertices into a cluster in a graph.
197 |
198 | In addition to the graph and the vertex ids, you can pass attributes
199 | for `node` and `edge` to apply common styling to the vertices included
200 | in the cluster, as well as the edges between two vertices both in the cluster.
201 |
202 | The difference between a cluster and a subgraph is that a cluster can also
203 | accept attributes to style the cluster, such as a border, background color,
204 | and custom label. These attributes can be passed as top-level attributes in
205 | the final keyword list argument to the function.
206 |
207 | ## Example
208 |
209 | iex> graph = Graph.new()
210 | iex> {graph, v1id} = Graph.add_vertex(graph, "start")
211 | iex> {graph, v2id} = Graph.add_vertex(graph, "end")
212 | iex> {_graph, cid} = Graph.add_cluster(
213 | ...> graph, [v1id, v2id],
214 | ...> color: "blue", label: "cluster0",
215 | ...> node: [shape: "triangle"],
216 | ...> edge: [style: "dotted"]
217 | ...> )
218 | iex> cid
219 | "cluster0"
220 |
221 | In `.dot` notation a cluster is specified, as opposed to a subgraph, by
222 | giving the cluster an ID that begins with `"cluster"` as seen in the example
223 | above. Contrast with `Graphvix.Graph.add_subgraph/3`.
224 |
225 | """
226 | def add_cluster(graph, vertex_ids, properties \\ []) do
227 | _add_subgraph(graph, vertex_ids, properties, true)
228 | end
229 |
230 | @doc """
231 | Add a vertex built from a `Graphvix.Record` to the graph.
232 |
233 | iex> graph = Graph.new()
234 | iex> record = Record.new(["a", "b", "c"])
235 | iex> {_graph, rid} = Graph.add_record(graph, record)
236 | iex> rid
237 | [:"$v" | 0]
238 |
239 | See `Graphvix.Record` for details on `Graphvix.Record.new/2`
240 | and the complete module API.
241 | """
242 | def add_record(graph, record) do
243 | label = Record.to_label(record)
244 | attributes = Keyword.put(record.properties, :shape, "record")
245 | add_vertex(graph, label, attributes)
246 | end
247 |
248 | @doc """
249 | Add a vertex built from a `Graphvix.HTMLRecord` to the graph.
250 |
251 | iex> graph = Graph.new()
252 | iex> record = HTMLRecord.new([
253 | ...> HTMLRecord.tr([
254 | ...> HTMLRecord.td("start"),
255 | ...> HTMLRecord.td("middle"),
256 | ...> HTMLRecord.td("end"),
257 | ...> ])
258 | ...> ])
259 | iex> {_graph, rid} = Graph.add_html_record(graph, record)
260 | iex> rid
261 | [:"$v" | 0]
262 |
263 | See `Graphvix.HTMLRecord` for details on `Graphvix.HTMLRecord.new/2`
264 | and the complete module API.
265 | """
266 | def add_html_record(graph, record) do
267 | label = HTMLRecord.to_label(record)
268 | attributes = [shape: "plaintext"]
269 | add_vertex(graph, label, attributes)
270 | end
271 |
272 | @doc """
273 | Converts a graph to its representation using `.dot` syntax.
274 |
275 | ## Example
276 |
277 | iex> graph = Graph.new(node: [shape: "triangle"], edge: [color: "green"], graph: [size: "4x4"])
278 | iex> {graph, vid} = Graph.add_vertex(graph, "a")
279 | iex> {graph, vid2} = Graph.add_vertex(graph, "b")
280 | iex> {graph, vid3} = Graph.add_vertex(graph, "c")
281 | iex> {graph, eid} = Graph.add_edge(graph, vid, vid2)
282 | iex> {graph, eid2} = Graph.add_edge(graph, vid, vid3)
283 | iex> {graph, clusterid} = Graph.add_cluster(graph, [vid, vid2])
284 | iex> Graph.to_dot(graph)
285 | ~S(digraph G {
286 |
287 | size="4x4"
288 |
289 | node [shape="triange"]
290 | edge [color="green"]
291 |
292 | subgraph cluster0 {
293 | v0 [label="a"]
294 | v1 [label="b"]
295 |
296 | v0 -> v1
297 | }
298 |
299 | v2 [label="c"]
300 |
301 | v1 -> v2
302 |
303 | })
304 |
305 | For more expressive examples, see the `.ex` and `.dot` files in the `examples/` directory of
306 | Graphvix's source code.
307 | """
308 | def to_dot(graph) do
309 | [
310 | "digraph G {",
311 | graph_properties_to_dot(graph),
312 | global_properties_to_dot(graph),
313 | subgraphs_to_dot(graph),
314 | vertices_to_dot(graph),
315 | edges_to_dot(graph),
316 | "}"
317 | ]
318 | |> Enum.reject(&is_nil/1)
319 | |> Enum.join("\n\n")
320 | end
321 |
322 | @doc """
323 | Writes a `Graph` to a named file in `.dot` format
324 |
325 | ```
326 | iex> Graph.write(graph, "my_graph")
327 | ```
328 |
329 | will write a file named `"my_graph.dot"` to your current working directory.
330 |
331 | `filename` works as expected in Elixir. Filenames beginning with `/` define
332 | an absolute path on your file system. Filenames otherwise define a path relative
333 | to your current working directory.
334 | """
335 | def write(graph, filename) do
336 | File.write(filename <> ".dot", to_dot(graph))
337 | end
338 |
339 | @doc """
340 | Writes the graph to a `.dot` file and compiles it to the specified output
341 | format (defaults to `.png`).
342 |
343 | The following code creates the files `"graph_one.dot"` and `"graph_one.png"`
344 | in your current working directory.
345 |
346 | ```
347 | iex> Graph.compile(graph, "graph_one")
348 | ```
349 |
350 | This code creates the files `"graph_one.dot"` and `"graph_one.pdf"`.
351 |
352 | ```
353 | iex> Graph.compile(graph, "graph_one", :pdf)
354 | ```
355 |
356 | `filename` works as expected in Elixir. Filenames beginning with `/` define
357 | an absolute path on your file system. Filenames otherwise define a path relative
358 | to your current working directory.
359 | """
360 | def compile(graph, filename, format \\ :png) do
361 | :ok = write(graph, filename)
362 |
363 | {output, 0} =
364 | System.cmd(@compiler, [
365 | "-T",
366 | "#{format}",
367 | filename <> ".dot",
368 | "-o",
369 | filename <> ".#{format}"
370 | ])
371 |
372 | {:ok, String.trim(output)}
373 | end
374 |
375 | @doc """
376 | Write a graph to file, compile it, and open the resulting image in your
377 | system's default image viewer.
378 |
379 | The following code will write the contents of `graph` to `"graph_one.dot"`,
380 | compile the file to `"graph_one.png"` and open it.
381 |
382 | ```
383 | iex> Graph.show(graph, "graph_one")
384 | ```
385 |
386 | `filename` works as expected in Elixir. Filenames beginning with `/` define
387 | an absolute path on your file system. Filenames otherwise define a path relative
388 | to your current working directory.
389 | """
390 | def show(graph, filename) do
391 | :ok = write(graph, filename <> ".dot")
392 | :ok = compile(graph, filename)
393 | {_, 0} = System.cmd("open", [filename <> ".png"])
394 | :ok
395 | end
396 |
397 | @doc """
398 | Adds a top-level graph property.
399 |
400 | These attributes affect the overall layout of the graph at a high level.
401 | Use `set_global_properties/3` to modify the global styling for vertices
402 | and edges.
403 |
404 | ## Example
405 |
406 | iex> graph = Graph.new()
407 | iex> graph.graph_properties
408 | []
409 | iex> graph = Graph.set_graph_property(graph, :rank_direction, "RL")
410 | iex> graph.graph_properties
411 | [
412 | rank_direction: "RL"
413 | ]
414 |
415 | """
416 | def set_graph_property(graph, key, value) do
417 | new_properties = Keyword.put(graph.graph_properties, key, value)
418 | %{graph | graph_properties: new_properties}
419 | end
420 |
421 | @doc """
422 | Sets a property for a vertex or edge that will apply to all vertices or edges
423 | in the graph.
424 |
425 | *NB* `:digraph` uses `vertex` to define the discrete points in
426 | a graph that are connected via edges, while Graphviz and DOT use the word
427 | `node`. `Graphvix` attempts to use "vertex" when the context is constructing
428 | the data for the graph, and "node" in the context of formatting and printing
429 | the graph.
430 |
431 | ## Example
432 |
433 | ```
434 | iex> graph = Graph.new()
435 | iex> {graph, vid} = Graph.add_vertex(graph, "label")
436 | iex> graph = Graph.set_global_property(graph, :node, shape: "triangle")
437 | ```
438 |
439 | When the graph is drawn, the vertex whose id is `vid`, and any other vertices
440 | added to the graph, will have a triangle shape.
441 |
442 | Global properties are overwritten by properties added by a subgraph or cluster:
443 |
444 | ```
445 | {graph, subgraph_id} = Graph.add_subgraph(graph, [vid], shape: "hexagon")
446 | ```
447 |
448 | Now when the graph is drawn the vertex `vid` will have a hexagon shape.
449 |
450 | Properties written directly to a vertex or edge have the highest priority
451 | of all. The vertex created below will have a circle shape despite the global
452 | property set on `graph`.
453 |
454 | ```
455 | {graph, vid2} = Graph.add_vertex(graph, "this is a circle!")
456 | ```
457 |
458 | """
459 | def set_global_properties(graph, attr_for, attrs \\ []) do
460 | Enum.reduce(attrs, graph, fn {k, v}, g ->
461 | _set_global_property(g, attr_for, [{k, v}])
462 | end)
463 | end
464 |
465 | ## PRIVATE
466 |
467 | defp _set_global_property(graph, attr_for, [{key, value}]) do
468 | properties = Keyword.get(graph.global_properties, attr_for)
469 | new_props = Keyword.put(properties, key, value)
470 | new_properties = Keyword.put(graph.global_properties, attr_for, new_props)
471 | %{graph | global_properties: new_properties}
472 | end
473 |
474 | defp subgraphs_to_dot(graph) do
475 | case graph.subgraphs do
476 | [] ->
477 | nil
478 |
479 | subgraphs ->
480 | subgraphs
481 | |> Enum.map_join("\n\n", &Graphvix.Subgraph.to_dot(&1, graph))
482 | end
483 | end
484 |
485 | defp vertices_to_dot(graph) do
486 | [vtab, _, _] = digraph_tables(graph)
487 |
488 | elements_to_dot(vtab, fn {vid = [_ | id], attributes} ->
489 | case in_a_subgraph?(vid, graph) do
490 | true ->
491 | nil
492 |
493 | false ->
494 | [
495 | "v#{id}",
496 | attributes_to_dot(attributes)
497 | ]
498 | |> compact()
499 | |> Enum.join(" ")
500 | |> indent()
501 | end
502 | end)
503 | end
504 |
505 | defp edge_side_with_port(v_id, nil), do: "v#{v_id}"
506 | defp edge_side_with_port(v_id, port), do: "v#{v_id}:#{port}"
507 |
508 | defp edges_to_dot(graph) do
509 | [_, etab, _] = digraph_tables(graph)
510 |
511 | elements_to_dot(etab, fn edge = {_, [:"$v" | v1], [:"$v" | v2], attributes} ->
512 | case edge in edges_contained_in_subgraphs(graph) do
513 | true ->
514 | nil
515 |
516 | false ->
517 | v_out = edge_side_with_port(v1, Keyword.get(attributes, :outport))
518 | v_in = edge_side_with_port(v2, Keyword.get(attributes, :inport))
519 | attributes = attributes |> Keyword.delete(:outport) |> Keyword.delete(:inport)
520 |
521 | ["#{v_out} -> #{v_in}", attributes_to_dot(attributes)]
522 | |> compact()
523 | |> Enum.join(" ")
524 | |> indent()
525 | end
526 | end)
527 | end
528 |
529 | defp get_and_increment_vertex_id(graph) do
530 | [_, _, ntab] = digraph_tables(graph)
531 | [{:"$vid", next_id}] = :ets.lookup(ntab, :"$vid")
532 | true = :ets.delete(ntab, :"$vid")
533 | true = :ets.insert(ntab, {:"$vid", next_id + 1})
534 | next_id
535 | end
536 |
537 | defp get_and_increment_subgraph_id(graph) do
538 | [_, _, ntab] = digraph_tables(graph)
539 | [{:"$sid", next_id}] = :ets.lookup(ntab, :"$sid")
540 | true = :ets.delete(ntab, :"$sid")
541 | true = :ets.insert(ntab, {:"$sid", next_id + 1})
542 | next_id
543 | end
544 |
545 | defp in_a_subgraph?(vertex_id, graph) do
546 | vertex_id in vertex_ids_in_subgraphs(graph)
547 | end
548 |
549 | defp vertex_ids_in_subgraphs(%__MODULE__{subgraphs: subgraphs}) do
550 | Enum.reduce(subgraphs, [], fn c, acc ->
551 | acc ++ c.vertex_ids
552 | end)
553 | end
554 |
555 | defp edges_contained_in_subgraphs(graph = %__MODULE__{subgraphs: subgraphs}) do
556 | [_, etab, _] = digraph_tables(graph)
557 | edges = :ets.tab2list(etab)
558 |
559 | Enum.filter(edges, fn {_, vid1, vid2, _} ->
560 | Enum.any?(subgraphs, fn subgraph ->
561 | Graphvix.Subgraph.both_vertices_in_subgraph?(subgraph.vertex_ids, vid1, vid2)
562 | end)
563 | end)
564 | end
565 |
566 | defp graph_properties_to_dot(%{graph_properties: []}), do: nil
567 |
568 | defp graph_properties_to_dot(%{graph_properties: properties}) do
569 | properties
570 | |> Enum.map_join("\n", fn {k, v} ->
571 | attribute_to_dot(k, v)
572 | end)
573 | |> indent
574 | end
575 |
576 | defp _add_subgraph(graph, vertex_ids, properties, is_cluster) do
577 | next_id = get_and_increment_subgraph_id(graph)
578 | subgraph = Graphvix.Subgraph.new(next_id, vertex_ids, is_cluster, properties)
579 | new_graph = %{graph | subgraphs: graph.subgraphs ++ [subgraph]}
580 | {new_graph, subgraph.id}
581 | end
582 | end
583 |
--------------------------------------------------------------------------------
/lib/graphvix/html_record.ex:
--------------------------------------------------------------------------------
1 | defmodule Graphvix.HTMLRecord do
2 | @moduledoc """
3 | Models a graph vertex that uses HTML to generate a table-shaped record.
4 |
5 | # Table structure
6 |
7 | The Graphviz API allows the basic table-related HTML elements:
8 |
9 | * ``
10 | * ``
11 | * ``
12 |
13 | and the `Graphvix` API provides the parallel functions:
14 |
15 | * `new/2`
16 | * `tr/1`
17 | * `td/2`
18 |
19 | ## Example
20 |
21 | iex> import Graphvix.HTMLRecord, only: [tr: 1, td: 1, td: 2]
22 | iex> record = HTMLRecord.new([
23 | iex> tr([
24 | ...> td("a"),
25 | ...> td("b")
26 | ...> ]),
27 | ...> tr([
28 | ...> td("c"),
29 | ...> td("d")
30 | ...> ]),
31 | ...> ])
32 | iex> HTMLRecord.to_label(record)
33 | ~S(
34 |
35 | a |
36 | b |
37 |
38 |
39 | c |
40 | d |
41 |
42 | )
43 |
44 | # Ports
45 |
46 | As with `Graphvix.Record` vertices, port names can be attached to cells. With
47 | `HTMLRecord` vertices, this is done by passing a `:port` key as one of the
48 | attributes in the second argument keyword list for `td/2`.
49 |
50 | iex> import Graphvix.HTMLRecord, only: [tr: 1, td: 1, td: 2]
51 | iex> record = HTMLRecord.new([
52 | iex> tr([td("a"), td("b")]),
53 | ...> tr([td("c", port: "port_c"), td("d")]),
54 | ...> ])
55 | iex> HTMLRecord.to_label(record)
56 | ~S(
57 |
58 | a |
59 | b |
60 |
61 |
62 | c |
63 | d |
64 |
65 | )
66 |
67 | In addition to `:port`, values for existing HTML keys
68 |
69 | * `border`
70 | * `cellpadding`
71 | * `cellspacing`
72 |
73 | can be added to cells, and
74 |
75 | * `border`
76 | * `cellborder`
77 | * `cellpadding`
78 | * `cellspacing`
79 |
80 | can be added to the table at the top-level to style the table and cells.
81 |
82 | # Text formatting
83 |
84 | Aside from structuring the table, two elements are available for formatting
85 | the content of the cells
86 |
87 | * ``
88 | * ` `
89 |
90 | with corresponding `Graphvix.HTMLRecord` functions
91 |
92 | * `font/2`
93 | * `br/0`
94 |
95 | In addition to contents as its first argument, `font/2` can take a keyword list
96 | of properties as its optional second argument.
97 |
98 | iex> import Graphvix.HTMLRecord, only: [tr: 1, td: 1, td: 2, br: 0, font: 2]
99 | iex> record = HTMLRecord.new([
100 | iex> tr([td("a"), td(["b", br(), font("B", color: "red", point_size: 100)])]),
101 | ...> tr([td("c"), td("d")]),
102 | ...> ])
103 | iex> HTMLRecord.to_label(record)
104 | ~S(
105 |
106 | a |
107 | b B |
108 |
109 |
110 | c |
111 | d |
112 |
113 | )
114 |
115 |
116 | While maintaining proper nesting (each element contains both opening and closing
117 | tags within its enclosing element), these elements may be nested as desired,
118 | including nesting entire tables inside of cells.
119 |
120 | """
121 |
122 | defstruct rows: [],
123 | attributes: []
124 |
125 | @type t :: %__MODULE__{
126 | rows: [__MODULE__.tr()],
127 | attributes: keyword()
128 | }
129 | @type tr :: %{cells: __MODULE__.cells()}
130 |
131 | @type br :: %{tag: :br}
132 | @type font :: %{tag: :font, cell: __MODULE__.one_or_more_cells(), attributes: keyword()}
133 | @type td :: %{label: __MODULE__.one_or_more_cells(), attributes: keyword()}
134 |
135 | @type cell ::
136 | String.t()
137 | | __MODULE__.br()
138 | | __MODULE__.font()
139 | | __MODULE__.td()
140 | | __MODULE__.t()
141 |
142 | @type cells :: [__MODULE__.cell()]
143 |
144 | @type one_or_more_cells :: __MODULE__.cell() | [__MODULE__.cell()]
145 |
146 | alias Graphvix.HTMLRecord
147 | import Graphvix.DotHelpers, only: [indent: 1]
148 |
149 | @doc """
150 | Returns a new `HTMLRecord` which can be turned into an HTML table vertex.
151 |
152 | It takes two arguments. The first is a list of table rows all returned from
153 | the `tr/1` function.
154 |
155 | The second is an optional keyword list of attributes to apply to the table as
156 | a whole. Valid keys for this list are:
157 |
158 | * `align`
159 | * `bgcolor`
160 | * `border`
161 | * `cellborder`
162 | * `cellpadding`
163 | * `cellspacing`
164 | * `color`
165 | * `columns`
166 | * `fixedsize`
167 | * `gradientangle`
168 | * `height`
169 | * `href`
170 | * `id`
171 | * `port`
172 | * `rows`
173 | * `sides`
174 | * `style`
175 | * `target`
176 | * `title`
177 | * `tooltip`
178 | * `valign`
179 | * `width`
180 |
181 | ## Example
182 |
183 | iex> import HTMLRecord, only: [tr: 1, td: 1]
184 | iex> HTMLRecord.new([
185 | ...> tr([
186 | ...> td("a"),
187 | ...> td("b")
188 | ...> ]),
189 | ...> tr([
190 | ...> td("c"),
191 | ...> td("d")
192 | ...> ])
193 | ...> ], border: 1, cellspacing: 0, cellborder: 1)
194 | %HTMLRecord{
195 | rows: [
196 | %{cells: [
197 | %{label: "a", attributes: []},
198 | %{label: "b", attributes: []},
199 | ]},
200 | %{cells: [
201 | %{label: "c", attributes: []},
202 | %{label: "d", attributes: []},
203 | ]}
204 | ],
205 | attributes: [
206 | border: 1,
207 | cellspacing: 0,
208 | cellborder: 1
209 | ]
210 | }
211 |
212 | """
213 | def new(rows, attributes \\ []) when is_list(rows) do
214 | %__MODULE__{rows: rows, attributes: attributes}
215 | end
216 |
217 | @doc """
218 | A helper method to generate a row of a table.
219 |
220 | It takes a single argument, which is a list of cells returned by the `td/2`
221 | helper function.
222 | """
223 | def tr(cells) when is_list(cells) do
224 | %{cells: cells}
225 | end
226 |
227 | @doc """
228 | A helper method to generate a single cell of a table.
229 |
230 | The first argument is the contents of the cell. It can be a plain string or
231 | a list of other elements.
232 |
233 | The second argument is an optional keyword list of attributes to apply to the
234 | cell. Valid keys include:
235 |
236 | * `align`
237 | * `balign`
238 | * `bgcolor`
239 | * `border`
240 | * `cellpadding`
241 | * `cellspacing`
242 | * `color`
243 | * `colspan`
244 | * `fixedsize`
245 | * `gradientangle`
246 | * `height`
247 | * `href`
248 | * `id`
249 | * `port`
250 | * `rowspan`
251 | * `sides`
252 | * `style`
253 | * `target`
254 | * `title`
255 | * `tooltip`
256 | * `valign`
257 | * `width`
258 |
259 | See the module documentation for `Graphvix.HTMLRecord` for usage examples in context.
260 | """
261 | def td(label, attributes \\ []) do
262 | %{label: label, attributes: attributes}
263 | end
264 |
265 | @doc """
266 | Creates a ` ` element as part of a cell in an `HTMLRecord`
267 |
268 | A helper method that creates a ` ` HTML element as part of a table cell.
269 |
270 | See the module documentation for `Graphvix.HTMLRecord` for usage examples in context.
271 | """
272 | def br, do: %{tag: :br}
273 |
274 | @doc """
275 | Creates a `` element as part of a cell in an `HTMLRecord`
276 |
277 | A helper method that creates a ` ` HTML element as part of a table cell.
278 |
279 | The first argument to `font/2` is the contents of the cell, which can itself
280 | be a plain string or a list of nested element functions.
281 |
282 | The second, optional argument is a keyword list of attributes to determine
283 | the formatting of the contents of the ``. Valid keys for this list are
284 |
285 | * `color`
286 | * `face`
287 | * `point_size`
288 |
289 | ## Example
290 |
291 | iex> HTMLRecord.font("a", color: "blue", face: "Arial", point_size: 10)
292 | %{tag: :font, cell: "a", attributes: [color: "blue", face: "Arial", point_size: 10]}
293 |
294 | """
295 | def font(cell, attributes \\ []) do
296 | %{tag: :font, cell: cell, attributes: attributes}
297 | end
298 |
299 | @doc """
300 | Converts an `HTMLRecord` struct into a valid HTML-like string.
301 |
302 | The resulting string can be passed to `Graphvix.Graph.add_vertex/3` as a label
303 | for a vertex.
304 |
305 | ## Example
306 |
307 | iex> import HTMLRecord, only: [tr: 1, td: 1]
308 | iex> record = HTMLRecord.new([
309 | ...> tr([
310 | ...> td("a"),
311 | ...> td("b")
312 | ...> ]),
313 | ...> tr([
314 | ...> td("c"),
315 | ...> td("d")
316 | ...> ])
317 | ...> ], border: 1, cellspacing: 0, cellborder: 1)
318 | iex> HTMLRecord.to_label(record)
319 | ~S(
320 |
321 | a |
322 | b |
323 |
324 |
325 | c |
326 | d |
327 |
328 | )
329 |
330 | """
331 | def to_label(%__MODULE__{rows: rows, attributes: attributes}) do
332 | [
333 | "",
334 | Enum.map(rows, &tr_to_label/1),
335 | " "
336 | ]
337 | |> List.flatten()
338 | |> Enum.join("\n")
339 | end
340 |
341 | ## Private
342 |
343 | defp tr_to_label(%{cells: cells}) do
344 | [
345 | "",
346 | Enum.map(cells, &td_to_label/1),
347 | " "
348 | ]
349 | |> List.flatten()
350 | |> Enum.join("\n")
351 | |> indent
352 | end
353 |
354 | defp td_to_label(%{label: label, attributes: attributes}) do
355 | [
356 | "",
357 | label_to_string(label),
358 | " | "
359 | ]
360 | |> Enum.join("")
361 | |> indent()
362 | end
363 |
364 | defp attributes_for_label(attributes) do
365 | case attributes do
366 | [] ->
367 | ""
368 |
369 | attrs ->
370 | " " <>
371 | (attrs
372 | |> Enum.map_join(" ", fn {k, v} ->
373 | ~s(#{hyphenize(k)}="#{v}")
374 | end))
375 | end
376 | end
377 |
378 | defp hyphenize(name) do
379 | name |> to_string |> String.replace("_", "-")
380 | end
381 |
382 | defp label_to_string(list) when is_list(list) do
383 | list |> Enum.map_join("", &label_to_string/1)
384 | end
385 |
386 | defp label_to_string(%{tag: :br}), do: " "
387 |
388 | defp label_to_string(%{tag: :font, cell: cell, attributes: attributes}) do
389 | [
390 | "",
391 | label_to_string(cell),
392 | ""
393 | ]
394 | |> Enum.join("")
395 | end
396 |
397 | defp label_to_string(table = %HTMLRecord{}) do
398 | HTMLRecord.to_label(table)
399 | end
400 |
401 | defp label_to_string(string) when is_bitstring(string), do: string
402 | end
403 |
--------------------------------------------------------------------------------
/lib/graphvix/record.ex:
--------------------------------------------------------------------------------
1 | defmodule Graphvix.Record do
2 | @moduledoc """
3 | Models a graph vertex that has a shape of `record`.
4 |
5 | A record's label can be a single string, a single row or column, or a nested
6 | alternation of rows and columns.
7 |
8 | Once a record is created by `Graphvix.Record.new/2` it can be added to a graph using
9 | `Graphvix.Graph.add_record/2`.
10 |
11 | See `new/2` for more complete usage examples.
12 |
13 | ## Example
14 |
15 | iex> import Record, only: [column: 1]
16 | iex> graph = Graph.new()
17 | iex> record = Record.new(["a", "B", column(["c", "D"])], color: "blue")
18 | iex> {graph, _rid} = Graph.add_record(graph, record)
19 | iex> Graph.to_dot(graph)
20 | ~s(digraph G {\\n\\n v0 [label="a | B | { c | D }",shape="record",color="blue"]\\n\\n})
21 |
22 | """
23 |
24 | defstruct body: nil,
25 | properties: []
26 |
27 | alias __MODULE__
28 | alias Graphvix.RecordSubset
29 |
30 | @type body :: String.t() | [any()] | RecordSubset.t()
31 | @type t :: %__MODULE__{body: Record.t(), properties: keyword()}
32 |
33 | @doc """
34 | Returns a new `Graphvix.Record` struct that can be added to a graph as a vertex.
35 |
36 | ## Examples
37 |
38 | A record's can be a simple text label
39 |
40 | iex> record = Record.new("just a plain text record")
41 | iex> Record.to_label(record)
42 | "just a plain text record"
43 |
44 | or it can be a single row or column of strings
45 |
46 | iex> import Record, only: [row: 1]
47 | iex> record = Record.new(row(["a", "b", "c"]))
48 | iex> Record.to_label(record)
49 | "a | b | c"
50 |
51 | iex> import Record, only: [column: 1]
52 | iex> record = Record.new(column(["a", "b", "c"]))
53 | iex> Record.to_label(record)
54 | "{ a | b | c }"
55 |
56 | or it can be a series of nested rows and columns
57 |
58 | iex> import Record, only: [row: 1, column: 1]
59 | iex> record = Record.new(
60 | ...> row([
61 | ...> "a",
62 | ...> column([
63 | ...> "b", "c", "d"
64 | ...> ]),
65 | ...> column([
66 | ...> "e",
67 | ...> "f",
68 | ...> row([
69 | ...> "g", "h", "i"
70 | ...> ])
71 | ...> ])
72 | ...> ])
73 | ...> )
74 | iex> Record.to_label(record)
75 | "a | { b | c | d } | { e | f | { g | h | i } }"
76 |
77 | passing a plain list defaults to a row
78 |
79 | iex> record = Record.new(["a", "b", "c"])
80 | iex> Record.to_label(record)
81 | "a | b | c"
82 |
83 | Each cell can contain a plain string, or a string with a port attached,
84 | allowing edges to be drawn directly to and from that cell, rather than the
85 | vertex. Ports are created by passing a tuple of the form `{port_name, label}`
86 |
87 | iex> record = Record.new(["a", {"port_b", "b"}])
88 | iex> Record.to_label(record)
89 | "a | b"
90 |
91 | A second, optional argument can be passed specifying other formatting and
92 | styling properties for the vertex.
93 |
94 | iex> record = Record.new(["a", {"port_b", "b"}, "c"], color: "blue")
95 | iex> graph = Graph.new()
96 | iex> {graph, _record_id} = Graph.add_record(graph, record)
97 | iex> Graph.to_dot(graph)
98 | ~s(digraph G {\\n\\n v0 [label="a | b | c",shape="record",color="blue"]\\n\\n})
99 |
100 |
101 | """
102 | def new(body, properties \\ [])
103 |
104 | def new(string, properties) when is_bitstring(string) do
105 | %__MODULE__{body: string, properties: properties}
106 | end
107 |
108 | def new(list, properties) when is_list(list) do
109 | %__MODULE__{body: Graphvix.RecordSubset.new(list), properties: properties}
110 | end
111 |
112 | def new(row_or_column = %Graphvix.RecordSubset{}, properties) do
113 | %__MODULE__{body: row_or_column, properties: properties}
114 | end
115 |
116 | @doc """
117 | A helper method that takes a list of cells and returns them as a row inside a
118 | `Graphvix.Record` struct.
119 |
120 | The list can consist of a mix of string labels or tuples of cell labels +
121 | port names.
122 |
123 | This function provides little functionality on its own. See the documentation
124 | for `Graphvix.Record.new/2` for usage examples in context.
125 | """
126 | def row(cells) do
127 | %RecordSubset{cells: cells, is_column: false}
128 | end
129 |
130 | @doc """
131 | A helper method that takes a list of cells and returns them as a column inside a
132 | `Graphvix.Record` struct.
133 |
134 | The list can consist of a mix of string labels or tuples of cell labels +
135 | port names.
136 |
137 | This function provides little functionality on its own. See the documentation
138 | for `Graphvix.Record.new/2` for usage examples in context.
139 | """
140 | def column(cells) do
141 | %RecordSubset{cells: cells, is_column: true}
142 | end
143 |
144 | @doc false
145 | def to_label(record)
146 |
147 | def to_label(%{body: string}) when is_bitstring(string) do
148 | string
149 | end
150 |
151 | def to_label(%{body: subset = %RecordSubset{}}) do
152 | RecordSubset.to_label(subset, true)
153 | end
154 | end
155 |
--------------------------------------------------------------------------------
/lib/graphvix/record/port.ex:
--------------------------------------------------------------------------------
1 | defmodule Graphvix.Record.Port do
2 | @moduledoc """
3 | [Internal] Models a `Graphvix.Record` cell with a port name attached.
4 |
5 | See `Graphvix.Record.new/2` for a more complete documentation of using
6 | ports with cells.
7 | """
8 |
9 | defstruct body: nil,
10 | port_name: nil
11 |
12 | @doc false
13 | def new(body, port_name) do
14 | %__MODULE__{body: body, port_name: port_name}
15 | end
16 |
17 | @doc false
18 | def to_label(port) do
19 | "<#{port.port_name}> #{port.body}"
20 | end
21 | end
22 |
--------------------------------------------------------------------------------
/lib/graphvix/record_subset.ex:
--------------------------------------------------------------------------------
1 | defmodule Graphvix.RecordSubset do
2 | @moduledoc """
3 | [Internal] Models a row or a column as part of a `Graphvix.Record` vertex.
4 |
5 | The functionality provided by this module is internal. See the documentation
6 | for `Graphvix.Record`.
7 | """
8 |
9 | defstruct cells: [],
10 | is_column: false
11 |
12 | alias __MODULE__
13 |
14 | @type t :: %__MODULE__{cells: [RecordSubset.cell()], is_column: boolean()}
15 | @type cell_with_port :: {String.t(), String.t()}
16 | @type cell :: String.t() | RecordSubset.cell_with_port() | RecordSubset.t()
17 |
18 | @doc false
19 | def new(cells, is_column \\ false) do
20 | %__MODULE__{cells: cells, is_column: is_column}
21 | end
22 |
23 | @doc false
24 | def to_label(subset, top_level \\ false)
25 |
26 | def to_label(%{cells: cells, is_column: false}, _top_level = true) do
27 | cells |> Enum.map_join(" | ", &_to_label/1)
28 | end
29 |
30 | def to_label(%{cells: cells}, _top_level) do
31 | "{ " <> (cells |> Enum.map_join(" | ", &_to_label/1)) <> " }"
32 | end
33 |
34 | defp _to_label(string) when is_bitstring(string), do: string
35 |
36 | defp _to_label({port, string}) do
37 | "<#{port}> #{string}"
38 | end
39 |
40 | defp _to_label(subset = %__MODULE__{}), do: __MODULE__.to_label(subset)
41 | end
42 |
--------------------------------------------------------------------------------
/lib/graphvix/subgraph.ex:
--------------------------------------------------------------------------------
1 | defmodule Graphvix.Subgraph do
2 | @moduledoc """
3 | [Internal] Models a subgraph or cluster for inclusion in a graph.
4 |
5 | The functions included in this module are for internal use only. See
6 |
7 | * `Graphvix.Graph.add_subgraph/3`
8 | * `Graphvix.Graph.add_cluster/3`
9 |
10 | for the public interface for creating and including subgraphs and clusters.
11 | """
12 |
13 | import Graphvix.DotHelpers
14 |
15 | defstruct id: nil,
16 | vertex_ids: [],
17 | global_properties: [node: [], edge: []],
18 | subgraph_properties: [],
19 | is_cluster: false
20 |
21 | @doc false
22 | def new(id, vertex_ids, is_cluster \\ false, properties \\ []) do
23 | node_properties = Keyword.get(properties, :node, [])
24 | edge_properties = Keyword.get(properties, :edge, [])
25 | subgraph_properties = properties |> Keyword.delete(:node) |> Keyword.delete(:edge)
26 |
27 | %Graphvix.Subgraph{
28 | id: id_prefix(is_cluster) <> "#{id}",
29 | is_cluster: is_cluster,
30 | vertex_ids: vertex_ids,
31 | global_properties: [
32 | node: node_properties,
33 | edge: edge_properties
34 | ],
35 | subgraph_properties: subgraph_properties
36 | }
37 | end
38 |
39 | @doc false
40 | def to_dot(subgraph, graph) do
41 | [vtab, _, _] = Graphvix.Graph.digraph_tables(graph)
42 | vertices_from_graph = :ets.tab2list(vtab)
43 |
44 | [
45 | "subgraph #{subgraph.id} {",
46 | global_properties_to_dot(subgraph),
47 | subgraph_properties_to_dot(subgraph),
48 | subgraph_vertices_to_dot(subgraph.vertex_ids, vertices_from_graph),
49 | subgraph_edges_to_dot(subgraph, graph),
50 | "}"
51 | ]
52 | |> List.flatten()
53 | |> compact()
54 | |> Enum.map_join("\n\n", &indent/1)
55 | end
56 |
57 | @doc false
58 | def subgraph_edges_to_dot(subgraph, graph) do
59 | subgraph
60 | |> edges_with_both_vertices_in_subgraph(graph)
61 | |> sort_elements_by_id()
62 | |> elements_to_dot(fn {_, [:"$v" | v1], [:"$v" | v2], attributes} ->
63 | "v#{v1} -> v#{v2} #{attributes_to_dot(attributes)}" |> String.trim() |> indent
64 | end)
65 | end
66 |
67 | @doc false
68 | def both_vertices_in_subgraph?(vertex_ids, vid1, vid2) do
69 | vid1 in vertex_ids && vid2 in vertex_ids
70 | end
71 |
72 | ## Private
73 |
74 | defp subgraph_vertices_to_dot(subgraph_vertex_ids, vertices_from_graph) do
75 | subgraph_vertex_ids
76 | |> vertices_in_this_subgraph(vertices_from_graph)
77 | |> sort_elements_by_id()
78 | |> elements_to_dot(fn {[_ | id], attributes} ->
79 | [
80 | "v#{id}",
81 | attributes_to_dot(attributes)
82 | ]
83 | |> compact
84 | |> Enum.join(" ")
85 | |> indent
86 | end)
87 | end
88 |
89 | defp vertices_in_this_subgraph(subgraph_vertex_ids, vertices_from_graph) do
90 | vertices_from_graph
91 | |> Enum.filter(fn {vid, _attributes} -> vid in subgraph_vertex_ids end)
92 | end
93 |
94 | defp subgraph_properties_to_dot(%{subgraph_properties: properties}) do
95 | properties
96 | |> Enum.map(fn {key, value} ->
97 | indent(attribute_to_dot(key, value))
98 | end)
99 | |> compact()
100 | |> return_joined_list_or_nil()
101 | end
102 |
103 | defp edges_with_both_vertices_in_subgraph(%{vertex_ids: vertex_ids}, graph) do
104 | [_, etab, _] = Graphvix.Graph.digraph_tables(graph)
105 | edges = :ets.tab2list(etab)
106 |
107 | Enum.filter(edges, fn {_, vid1, vid2, _} ->
108 | both_vertices_in_subgraph?(vertex_ids, vid1, vid2)
109 | end)
110 | end
111 |
112 | defp id_prefix(_is_cluster = true), do: "cluster"
113 | defp id_prefix(_is_cluster = false), do: "subgraph"
114 | end
115 |
--------------------------------------------------------------------------------
/mix.exs:
--------------------------------------------------------------------------------
1 | defmodule Graphvix.Mixfile do
2 | use Mix.Project
3 |
4 | @version "1.1.0"
5 | @maintainers [
6 | "Michael Berkowitz"
7 | ]
8 | @links %{
9 | github: "https://github.com/mikowitz/graphvix"
10 | }
11 |
12 | def project do
13 | [
14 | app: :graphvix,
15 | version: @version,
16 | description: "Elixir interface for Graphviz",
17 | package: [
18 | maintainers: @maintainers,
19 | licenses: ["MIT"],
20 | links: @links
21 | ],
22 | elixir: "~> 1.13",
23 | build_embedded: Mix.env() == :prod,
24 | start_permanent: Mix.env() == :prod,
25 | deps: deps()
26 | ]
27 | end
28 |
29 | # Configuration for the OTP application
30 | #
31 | # Type "mix help compile.app" for more information
32 | def application do
33 | [
34 | extra_applications: [:logger]
35 | ]
36 | end
37 |
38 | # Dependencies can be Hex packages:
39 | #
40 | # {:mydep, "~> 0.3.0"}
41 | #
42 | # Or git/path repositories:
43 | #
44 | # {:mydep, git: "https://github.com/elixir-lang/mydep.git", tag: "0.1.0"}
45 | #
46 | # Type "mix help deps" for more examples and options
47 | defp deps do
48 | [
49 | {:credo, "~> 1.7", only: [:dev, :test], runtime: false},
50 | {:ex_doc, "~> 0.31", only: :dev, runtime: false},
51 | {:mix_test_watch, "~> 1.0", only: [:dev, :test], runtime: false},
52 | {:stream_data, "~> 1.0", only: :test}
53 | ]
54 | end
55 | end
56 |
--------------------------------------------------------------------------------
/mix.lock:
--------------------------------------------------------------------------------
1 | %{
2 | "bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"},
3 | "credo": {:hex, :credo, "1.7.6", "b8f14011a5443f2839b04def0b252300842ce7388f3af177157c86da18dfbeea", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "146f347fb9f8cbc5f7e39e3f22f70acbef51d441baa6d10169dd604bfbc55296"},
4 | "earmark_parser": {:hex, :earmark_parser, "1.4.39", "424642f8335b05bb9eb611aa1564c148a8ee35c9c8a8bba6e129d51a3e3c6769", [:mix], [], "hexpm", "06553a88d1f1846da9ef066b87b57c6f605552cfbe40d20bd8d59cc6bde41944"},
5 | "ex_doc": {:hex, :ex_doc, "0.32.2", "f60bbeb6ccbe75d005763e2a328e6f05e0624232f2393bc693611c2d3ae9fa0e", [:mix], [{:earmark_parser, "~> 1.4.39", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "a4480305cdfe7fdfcbb77d1092c76161626d9a7aa4fb698aee745996e34602df"},
6 | "file_system": {:hex, :file_system, "1.0.0", "b689cc7dcee665f774de94b5a832e578bd7963c8e637ef940cd44327db7de2cd", [:mix], [], "hexpm", "6752092d66aec5a10e662aefeed8ddb9531d79db0bc145bb8c40325ca1d8536d"},
7 | "jason": {:hex, :jason, "1.4.1", "af1504e35f629ddcdd6addb3513c3853991f694921b1b9368b0bd32beb9f1b63", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "fbb01ecdfd565b56261302f7e1fcc27c4fb8f32d56eab74db621fc154604a7a1"},
8 | "makeup": {:hex, :makeup, "1.1.2", "9ba8837913bdf757787e71c1581c21f9d2455f4dd04cfca785c70bbfff1a76a3", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "cce1566b81fbcbd21eca8ffe808f33b221f9eee2cbc7a1706fc3da9ff18e6cac"},
9 | "makeup_elixir": {:hex, :makeup_elixir, "0.16.2", "627e84b8e8bf22e60a2579dad15067c755531fea049ae26ef1020cad58fe9578", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "41193978704763f6bbe6cc2758b84909e62984c7752b3784bd3c218bb341706b"},
10 | "makeup_erlang": {:hex, :makeup_erlang, "1.0.0", "6f0eff9c9c489f26b69b61440bf1b238d95badae49adac77973cbacae87e3c2e", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "ea7a9307de9d1548d2a72d299058d1fd2339e3d398560a0e46c27dab4891e4d2"},
11 | "mix_test_watch": {:hex, :mix_test_watch, "1.2.0", "1f9acd9e1104f62f280e30fc2243ae5e6d8ddc2f7f4dc9bceb454b9a41c82b42", [:mix], [{:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}], "hexpm", "278dc955c20b3fb9a3168b5c2493c2e5cffad133548d307e0a50c7f2cfbf34f6"},
12 | "nimble_parsec": {:hex, :nimble_parsec, "1.4.0", "51f9b613ea62cfa97b25ccc2c1b4216e81df970acd8e16e8d1bdc58fef21370d", [:mix], [], "hexpm", "9c565862810fb383e9838c1dd2d7d2c437b3d13b267414ba6af33e50d2d1cf28"},
13 | "stream_data": {:hex, :stream_data, "1.0.0", "c1380747a4650902732696861d5cb66ad3cb1cc93f31c2c8498bf87cddbabe2d", [:mix], [], "hexpm", "acd53e27c66c617d466f42ec77a7f59e5751f6051583c621ccdb055b9690435d"},
14 | }
15 |
--------------------------------------------------------------------------------
/test/graphvix/dot_helpers_test.exs:
--------------------------------------------------------------------------------
1 | defmodule Graphvix.DotHelpersTest do
2 | use ExUnit.Case, async: true
3 |
4 | alias Graphvix.{DotHelpers, Graph}
5 | doctest DotHelpers
6 | end
7 |
--------------------------------------------------------------------------------
/test/graphvix/graph_test.exs:
--------------------------------------------------------------------------------
1 | defmodule Graphvix.GraphTest do
2 | use ExUnit.Case, async: true
3 | use ExUnitProperties
4 |
5 | alias Graphvix.{Graph, HTMLRecord, Record}
6 |
7 | doctest Graph,
8 | except: [
9 | new: 1,
10 | digraph_tables: 1,
11 | to_dot: 1,
12 | write: 2,
13 | compile: 3,
14 | show: 2,
15 | set_global_properties: 3
16 | ]
17 |
18 | import HTMLRecord, only: [tr: 1, td: 1, td: 2]
19 |
20 | property "generating a graph with a vertex" do
21 | check all(label <- string(:ascii, min_length: 3)) do
22 | graph = Graph.new()
23 | {graph, _vid} = Graph.add_vertex(graph, label, color: "blue")
24 |
25 | assert Graph.to_dot(graph) ==
26 | """
27 | digraph G {
28 |
29 | v0 [label="#{label}",color="blue"]
30 |
31 | }
32 | """
33 | |> String.trim()
34 | end
35 | end
36 |
37 | property "generating a graph with a record node" do
38 | check all(
39 | labels <- list_of(string(:ascii, min_length: 3), min_length: 2, max_length: 5),
40 | label <- string(:ascii, min_length: 3),
41 | color <- string(:ascii, min_length: 3)
42 | ) do
43 | record = Record.new(labels, color: "blue")
44 | graph = Graph.new()
45 | {graph, v0} = Graph.add_record(graph, record)
46 | {graph, v1} = Graph.add_vertex(graph, label, color: color)
47 | {graph, _eid} = Graph.add_edge(graph, v0, v1)
48 |
49 | assert Graph.to_dot(graph) ==
50 | """
51 | digraph G {
52 |
53 | v0 [label="#{Enum.join(labels, " | ")}",shape="record",color="blue"]
54 | v1 [label="#{label}",color="#{color}"]
55 |
56 | v0 -> v1
57 |
58 | }
59 | """
60 | |> String.trim()
61 | end
62 | end
63 |
64 | property "generating a graph with edges to record ports" do
65 | check all([l1, l2, p1, l3] <- list_of(string(:ascii, min_length: 3), length: 4)) do
66 | graph = Graph.new()
67 | record = Record.new([l1, {p1, l2}])
68 | {graph, v0} = Graph.add_record(graph, record)
69 | {graph, v1} = Graph.add_vertex(graph, l3)
70 | {graph, _eid} = Graph.add_edge(graph, {v0, p1}, v1)
71 |
72 | assert Graph.to_dot(graph) ==
73 | """
74 | digraph G {
75 |
76 | v0 [label="#{l1} | <#{p1}> #{l2}",shape="record"]
77 | v1 [label="#{l3}"]
78 |
79 | v0:#{p1} -> v1
80 |
81 | }
82 | """
83 | |> String.trim()
84 | end
85 | end
86 |
87 | property "generating a graph with edges to HTML record ports" do
88 | check all([l1, l2, p1, l3] <- list_of(string(:ascii, min_length: 3), length: 4)) do
89 | graph = Graph.new()
90 |
91 | record =
92 | HTMLRecord.new([
93 | tr([
94 | td(l1),
95 | td(l2, port: p1)
96 | ])
97 | ])
98 |
99 | {graph, v0} = Graph.add_html_record(graph, record)
100 | {graph, v1} = Graph.add_vertex(graph, l3)
101 | {graph, _eid} = Graph.add_edge(graph, {v0, p1}, v1)
102 |
103 | assert Graph.to_dot(graph) ==
104 | """
105 | digraph G {
106 |
107 | v0 [label=<
108 |
109 | #{l1} |
110 | #{l2} |
111 |
112 | >,shape="plaintext"]
113 | v1 [label="#{l3}"]
114 |
115 | v0:#{p1} -> v1
116 |
117 | }
118 | """
119 | |> String.trim()
120 | end
121 | end
122 |
123 | property "adding a subgraph" do
124 | check all(label <- string(:ascii, min_length: 3)) do
125 | graph = Graph.new()
126 | {graph, vid} = Graph.add_vertex(graph, label, color: "blue")
127 | {graph, _cid} = Graph.add_subgraph(graph, [vid])
128 |
129 | [subgraph] = graph.subgraphs
130 | assert subgraph.id == "subgraph0"
131 | assert subgraph.vertex_ids == [vid]
132 | end
133 | end
134 |
135 | property "adding a cluster" do
136 | check all(label <- string(:ascii, min_length: 3)) do
137 | graph = Graph.new()
138 | {graph, vid} = Graph.add_vertex(graph, label, color: "blue")
139 | {graph, _cid} = Graph.add_cluster(graph, [vid])
140 |
141 | [cluster] = graph.subgraphs
142 | assert cluster.id == "cluster0"
143 | assert cluster.vertex_ids == [vid]
144 | end
145 | end
146 |
147 | property "generating graphs with global properties" do
148 | check all(
149 | color <- string(:ascii, min_length: 3),
150 | color2 <- string(:ascii, min_length: 3),
151 | e_label <- string(:printable, min_length: 5)
152 | ) do
153 | graph = Graph.new()
154 | graph = Graph.set_global_properties(graph, :node, color: color)
155 | graph = Graph.set_global_properties(graph, :edge, color: color2, label: e_label)
156 |
157 | assert Graph.to_dot(graph) ==
158 | """
159 | digraph G {
160 |
161 | node [color="#{color}"]
162 | edge [label="#{e_label}",color="#{color2}"]
163 |
164 | }
165 | """
166 | |> String.trim()
167 | end
168 | end
169 |
170 | property "adding an edge" do
171 | check all(
172 | label1 <- string(:ascii, min_length: 3),
173 | label2 <- string(:ascii, min_length: 3)
174 | ) do
175 | graph = Graph.new()
176 | {graph, v1} = Graph.add_vertex(graph, label1)
177 | {graph, v2} = Graph.add_vertex(graph, label2)
178 | {graph, _e1} = Graph.add_edge(graph, v1, v2)
179 | {_, _, etab, _, _} = graph.digraph
180 | assert length(:ets.tab2list(etab)) == 1
181 | end
182 | end
183 |
184 | property "dot format for a graph with edges" do
185 | check all(
186 | label1 <- string(:ascii, min_length: 3),
187 | label2 <- string(:ascii, min_length: 3)
188 | ) do
189 | graph = Graph.new()
190 | {graph, v1} = Graph.add_vertex(graph, label1)
191 | {graph, v2} = Graph.add_vertex(graph, label2)
192 | {graph, _e1} = Graph.add_edge(graph, v1, v2, color: "blue")
193 |
194 | assert Graph.to_dot(graph) ==
195 | """
196 | digraph G {
197 |
198 | v0 [label="#{label1}"]
199 | v1 [label="#{label2}"]
200 |
201 | v0 -> v1 [color="blue"]
202 |
203 | }
204 | """
205 | |> String.trim()
206 | end
207 | end
208 |
209 | property "dot format for a graph with a subgraph" do
210 | check all(
211 | label1 <- string(:ascii, min_length: 3),
212 | label2 <- string(:ascii, min_length: 3)
213 | ) do
214 | graph = Graph.new()
215 | {graph, v1} = Graph.add_vertex(graph, label1)
216 | {graph, v2} = Graph.add_vertex(graph, label2)
217 | {graph, _e1} = Graph.add_edge(graph, v1, v2, color: "blue")
218 |
219 | {graph, _cid} =
220 | Graph.add_subgraph(graph, [v1], style: "filled", color: "blue", node: [shape: "Msquare"])
221 |
222 | assert Graph.to_dot(graph) ==
223 | """
224 | digraph G {
225 |
226 | subgraph subgraph0 {
227 |
228 | node [shape="Msquare"]
229 |
230 | style="filled"
231 | color="blue"
232 |
233 | v0 [label="#{label1}"]
234 |
235 | }
236 |
237 | v1 [label="#{label2}"]
238 |
239 | v0 -> v1 [color="blue"]
240 |
241 | }
242 | """
243 | |> String.trim()
244 | end
245 | end
246 |
247 | property "dot format for a graph with a cluster" do
248 | check all(
249 | label1 <- string(:ascii, min_length: 3),
250 | label2 <- string(:ascii, min_length: 3)
251 | ) do
252 | graph = Graph.new()
253 | {graph, v1} = Graph.add_vertex(graph, label1)
254 | {graph, v2} = Graph.add_vertex(graph, label2)
255 | {graph, _e1} = Graph.add_edge(graph, v1, v2, color: "blue")
256 |
257 | {graph, _cid} =
258 | Graph.add_cluster(graph, [v1], style: "filled", color: "blue", node: [shape: "Msquare"])
259 |
260 | assert Graph.to_dot(graph) ==
261 | """
262 | digraph G {
263 |
264 | subgraph cluster0 {
265 |
266 | node [shape="Msquare"]
267 |
268 | style="filled"
269 | color="blue"
270 |
271 | v0 [label="#{label1}"]
272 |
273 | }
274 |
275 | v1 [label="#{label2}"]
276 |
277 | v0 -> v1 [color="blue"]
278 |
279 | }
280 | """
281 | |> String.trim()
282 | end
283 | end
284 |
285 | property "dot format for a graph with clusters and subgraphs" do
286 | check all(
287 | label1 <- string(:ascii, min_length: 3),
288 | label2 <- string(:ascii, min_length: 3),
289 | label3 <- string(:ascii, min_length: 3),
290 | label4 <- string(:ascii, min_length: 3)
291 | ) do
292 | graph = Graph.new()
293 | {graph, v1} = Graph.add_vertex(graph, label1)
294 | {graph, v2} = Graph.add_vertex(graph, label2)
295 | {graph, v3} = Graph.add_vertex(graph, label3)
296 | {graph, v4} = Graph.add_vertex(graph, label4)
297 | {graph, _e} = Graph.add_edge(graph, v1, v2, color: "blue")
298 | {graph, _e} = Graph.add_edge(graph, v2, v3)
299 | {graph, _e} = Graph.add_edge(graph, v3, v4)
300 |
301 | {graph, _cid} =
302 | Graph.add_cluster(graph, [v1], style: "filled", color: "blue", node: [shape: "Msquare"])
303 |
304 | {graph, _cid} =
305 | Graph.add_subgraph(graph, [v2, v3], node: [shape: "square"], edge: [color: "green"])
306 |
307 | assert Graph.to_dot(graph) ==
308 | """
309 | digraph G {
310 |
311 | subgraph cluster0 {
312 |
313 | node [shape="Msquare"]
314 |
315 | style="filled"
316 | color="blue"
317 |
318 | v0 [label="#{label1}"]
319 |
320 | }
321 |
322 | subgraph subgraph1 {
323 |
324 | node [shape="square"]
325 | edge [color="green"]
326 |
327 | v1 [label="#{label2}"]
328 | v2 [label="#{label3}"]
329 |
330 | v1 -> v2
331 |
332 | }
333 |
334 | v3 [label="#{label4}"]
335 |
336 | v0 -> v1 [color="blue"]
337 | v2 -> v3
338 |
339 | }
340 | """
341 | |> String.trim()
342 | end
343 | end
344 |
345 | test ".write/2" do
346 | g = Graph.new()
347 |
348 | :ok = Graph.write(g, "g")
349 |
350 | {:ok, content} = File.read("g.dot")
351 |
352 | :ok = File.rm("g.dot")
353 |
354 | assert content ==
355 | """
356 | digraph G {
357 |
358 | }
359 | """
360 | |> String.trim()
361 | end
362 |
363 | test ".compile/2" do
364 | g = Graph.new()
365 |
366 | {:ok, output} = Graph.compile(g, "g")
367 |
368 | assert output == "-T png g.dot -o g.png"
369 | end
370 |
371 | test ".compile/3" do
372 | g = Graph.new()
373 |
374 | {:ok, output} = Graph.compile(g, "g", :pdf)
375 |
376 | assert output == "-T pdf g.dot -o g.pdf"
377 | end
378 | end
379 |
--------------------------------------------------------------------------------
/test/graphvix/html_record_test.exs:
--------------------------------------------------------------------------------
1 | defmodule Graphvix.HTMLRecordTest do
2 | use ExUnit.Case, async: true
3 | use ExUnitProperties
4 |
5 | alias Graphvix.HTMLRecord
6 | import HTMLRecord, only: [tr: 1, td: 1, td: 2]
7 | doctest HTMLRecord
8 |
9 | test "generating a basic HTML label" do
10 | cell1 = td("left")
11 | cell2 = td("mid dle")
12 | cell3 = td("right")
13 | row = tr([cell1, cell2, cell3])
14 |
15 | record = HTMLRecord.new([row])
16 |
17 | assert HTMLRecord.to_label(record) ==
18 | """
19 |
20 |
21 | left |
22 | mid dle |
23 | right |
24 |
25 |
26 | """
27 | |> String.trim()
28 | end
29 |
30 | test "generating an HTML label with col and rowspans" do
31 | row1_cell1 = td("hello world", rowspan: 3)
32 | row1_cell2 = td("b", colspan: 3)
33 | row1_cell3 = td("g", rowspan: 3)
34 | row1_cell4 = td("h", rowspan: 3)
35 |
36 | row1 =
37 | tr([
38 | row1_cell1,
39 | row1_cell2,
40 | row1_cell3,
41 | row1_cell4
42 | ])
43 |
44 | row2_cell1 = td("c")
45 | row2_cell2 = td("d")
46 | row2_cell3 = td("e")
47 |
48 | row2 =
49 | tr([
50 | row2_cell1,
51 | row2_cell2,
52 | row2_cell3
53 | ])
54 |
55 | row3_cell1 = td("f", colspan: 3)
56 |
57 | row3 = tr([row3_cell1])
58 |
59 | record = HTMLRecord.new([row1, row2, row3])
60 |
61 | assert HTMLRecord.to_label(record) ==
62 | """
63 |
64 |
65 | hello world |
66 | b |
67 | g |
68 | h |
69 |
70 |
71 | c |
72 | d |
73 | e |
74 |
75 |
76 | f |
77 |
78 |
79 | """
80 | |> String.trim()
81 | end
82 | end
83 |
--------------------------------------------------------------------------------
/test/graphvix/record_test.exs:
--------------------------------------------------------------------------------
1 | defmodule Graphvix.RecordTest do
2 | use ExUnit.Case, async: true
3 | use ExUnitProperties
4 |
5 | alias Graphvix.{Graph, Record, RecordSubset}
6 |
7 | doctest Record
8 |
9 | property "generating a simple record" do
10 | check all(label <- string(:ascii, min_length: 3)) do
11 | record = Record.new(label)
12 | assert Record.to_label(record) == label
13 | end
14 | end
15 |
16 | property "generating a basic record with a single row" do
17 | check all(labels <- list_of(string(:ascii, min_length: 3), min_length: 2, max_length: 5)) do
18 | record = Record.new(labels)
19 | assert Record.to_label(record) == Enum.join(labels, " | ")
20 | end
21 | end
22 |
23 | property "generating a record as a column" do
24 | check all(labels <- list_of(string(:ascii, min_length: 3), min_length: 2, max_length: 5)) do
25 | record = Record.new(RecordSubset.new(labels, true))
26 | assert Record.to_label(record) == "{ #{Enum.join(labels, " | ")} }"
27 | end
28 | end
29 |
30 | property "generating a nested record starting with a row" do
31 | check all([l1, l2, l3, l4, l5] <- list_of(string(:ascii, min_length: 3), length: 5)) do
32 | record =
33 | Record.new(
34 | RecordSubset.new([
35 | l1,
36 | RecordSubset.new([l2, l3, l4], true),
37 | l5
38 | ])
39 | )
40 |
41 | assert Record.to_label(record) == "#{l1} | { #{l2} | #{l3} | #{l4} } | #{l5}"
42 | end
43 | end
44 |
45 | property "multi-nested record" do
46 | check all(
47 | [l1, l2, l3, l4, l5, l6, l7, l8] <- list_of(string(:ascii, min_length: 3), length: 8)
48 | ) do
49 | record =
50 | Record.new(
51 | RecordSubset.new([
52 | l1,
53 | RecordSubset.new(
54 | [l2, RecordSubset.new([l3, l4, RecordSubset.new([l5, l6], true)]), l7],
55 | true
56 | ),
57 | l8
58 | ])
59 | )
60 |
61 | assert Record.to_label(record) ==
62 | "#{l1} | { #{l2} | { #{l3} | #{l4} | { #{l5} | #{l6} } } | #{l7} } | #{l8}"
63 | end
64 | end
65 |
66 | property "basic record with named ports" do
67 | check all(
68 | [l1, l2, l3] <- list_of(string(:ascii, min_length: 3), length: 3),
69 | port_name <- string(:ascii, min_length: 3)
70 | ) do
71 | record = Record.new(RecordSubset.new([l1, {port_name, l2}, l3]))
72 | assert Record.to_label(record) == "#{l1} | <#{port_name}> #{l2} | #{l3}"
73 | end
74 | end
75 |
76 | property "multi-nested record with ports" do
77 | check all(
78 | [l1, l2, l3, l4, l5, l6, l7, l8, p1, p2] <-
79 | list_of(string(:ascii, min_length: 3), length: 10)
80 | ) do
81 | record =
82 | Record.new(
83 | RecordSubset.new(
84 | [
85 | {p1, l1},
86 | RecordSubset.new([
87 | l2,
88 | RecordSubset.new([l3, l4, RecordSubset.new([l5, {p2, l6}])], true),
89 | l7
90 | ]),
91 | l8
92 | ],
93 | true
94 | )
95 | )
96 |
97 | assert Record.to_label(record) ==
98 | "{ <#{p1}> #{l1} | { #{l2} | { #{l3} | #{l4} | { #{l5} | <#{p2}> #{l6} } } | #{l7} } | #{l8} }"
99 | end
100 | end
101 | end
102 |
--------------------------------------------------------------------------------
/test/graphvix/subgraph_test.exs:
--------------------------------------------------------------------------------
1 | defmodule Graphvix.SubgraphTest do
2 | use ExUnit.Case, async: true
3 |
4 | alias Graphvix.Subgraph
5 | doctest Subgraph
6 | end
7 |
--------------------------------------------------------------------------------
/test/test_helper.exs:
--------------------------------------------------------------------------------
1 | ExUnit.start()
2 |
--------------------------------------------------------------------------------
|