nxpp
Header-only graph utilities on top of Boost Graph Library
Loading...
Searching...
No Matches
dot.hpp
Go to the documentation of this file.
1#pragma once
2
11#include "../graph.hpp"
12
13#include <any>
14#include <cctype>
15#include <filesystem>
16#include <fstream>
17#include <map>
18#include <optional>
19#include <sstream>
20#include <stdexcept>
21#include <string>
22#include <vector>
23
24namespace nxpp::viz {
25
27enum class DotLayout {
28 Dot,
29 Neato,
30 Fdp,
31 Sfdp,
32 Circo
33};
34
36struct DotOptions {
37 bool show_node_labels = true;
38 bool show_edge_labels = true;
39 bool show_weights = true;
40 bool show_edge_ids = false;
41 bool show_user_attrs = false;
42 std::map<std::string, std::string> graph_attrs;
43 std::optional<DotLayout> layout = std::nullopt;
44 std::string graph_name = "G";
45};
46
47namespace detail {
48
49inline std::string quote_dot_string(const std::string& value) {
50 std::string out = "\"";
51 for (char c : value) {
52 if (c == '\\' || c == '"') {
53 out += '\\';
54 }
55 out += c;
56 }
57 out += '"';
58 return out;
59}
60
61inline bool is_plain_dot_id(const std::string& value) {
62 if (value.empty()) {
63 return false;
64 }
65 const auto first = static_cast<unsigned char>(value.front());
66 if (!(std::isalpha(first) || value.front() == '_')) {
67 return false;
68 }
69 for (char c : value) {
70 const auto ch = static_cast<unsigned char>(c);
71 if (!(std::isalnum(ch) || c == '_')) {
72 return false;
73 }
74 }
75 return true;
76}
77
78inline bool is_dot_number(const std::string& value) {
79 bool has_digit = false;
80 bool has_exponent = false;
81 bool has_dot = false;
82
83 for (std::size_t i = 0; i < value.size(); ++i) {
84 const char c = value[i];
85 if (std::isdigit(static_cast<unsigned char>(c))) {
86 has_digit = true;
87 continue;
88 }
89 if (c == '-' && i == 0) {
90 continue;
91 }
92 if ((c == '+' || c == '-') && i > 0 && (value[i - 1] == 'e' || value[i - 1] == 'E')) {
93 continue;
94 }
95 if (c == '.' && !has_dot && !has_exponent) {
96 has_dot = true;
97 continue;
98 }
99 if ((c == 'e' || c == 'E') && has_digit && !has_exponent) {
100 has_exponent = true;
101 has_digit = false;
102 continue;
103 }
104 return false;
105 }
106
107 return has_digit;
108}
109
110inline std::string graph_id(const std::string& value) {
111 return is_plain_dot_id(value) ? value : quote_dot_string(value);
112}
113
114inline std::string attr_value(const std::string& value) {
115 return is_plain_dot_id(value) || is_dot_number(value) ? value : quote_dot_string(value);
116}
117
118inline const char* dot_layout_name(DotLayout layout) {
119 switch (layout) {
120 case DotLayout::Dot:
121 return "dot";
122 case DotLayout::Neato:
123 return "neato";
124 case DotLayout::Fdp:
125 return "fdp";
126 case DotLayout::Sfdp:
127 return "sfdp";
128 case DotLayout::Circo:
129 return "circo";
130 }
131 return "dot";
132}
133
134using DotAttrList = std::vector<std::pair<std::string, std::string>>;
135
136inline bool has_dot_attr(const DotAttrList& attrs, const std::string& key) {
137 return std::any_of(
138 attrs.begin(),
139 attrs.end(),
140 [&](const auto& attr) { return attr.first == key; }
141 );
142}
143
144inline void append_dot_attr(DotAttrList& attrs, const std::string& key, const std::string& value) {
145 if (!has_dot_attr(attrs, key)) {
146 attrs.emplace_back(key, value);
147 }
148}
149
150inline std::string format_dot_attrs(const DotAttrList& attrs) {
151 std::string out;
152 for (const auto& [key, value] : attrs) {
153 if (!out.empty()) {
154 out += " ";
155 }
156 out += graph_id(key) + "=" + attr_value(value);
157 }
158 return out;
159}
160
161template <typename T>
162std::string to_string(const T& value) {
163 std::ostringstream out;
164 out << value;
165 return out.str();
166}
167
168template <typename T>
169std::optional<std::string> any_attr_as_string(const std::any& value) {
170 if (const auto* typed = std::any_cast<T>(&value)) {
171 return to_string(*typed);
172 }
173 return std::nullopt;
174}
175
176inline std::string any_attr_value(const std::any& value) {
177 if (const auto* typed = std::any_cast<std::string>(&value)) {
178 return *typed;
179 }
180 if (const auto* typed = std::any_cast<bool>(&value)) {
181 return *typed ? "true" : "false";
182 }
183
184 if (auto converted = any_attr_as_string<short>(value)) return *converted;
185 if (auto converted = any_attr_as_string<unsigned short>(value)) return *converted;
186 if (auto converted = any_attr_as_string<int>(value)) return *converted;
187 if (auto converted = any_attr_as_string<unsigned int>(value)) return *converted;
188 if (auto converted = any_attr_as_string<long>(value)) return *converted;
189 if (auto converted = any_attr_as_string<unsigned long>(value)) return *converted;
190 if (auto converted = any_attr_as_string<long long>(value)) return *converted;
191 if (auto converted = any_attr_as_string<unsigned long long>(value)) return *converted;
192 if (auto converted = any_attr_as_string<float>(value)) return *converted;
193 if (auto converted = any_attr_as_string<double>(value)) return *converted;
194 if (auto converted = any_attr_as_string<long double>(value)) return *converted;
195
196 throw std::runtime_error("DOT export failed: unsupported user attribute type.");
197}
198
199} // namespace detail
200
209template <
210 typename NodeID,
211 typename EdgeWeight,
212 bool Directed,
213 bool Multi,
214 bool Weighted,
215 typename OutEdgeSelector,
216 typename VertexSelector
217>
218std::string to_dot(
220 const DotOptions& options = {}
221) {
222 const char* graph_keyword = Directed ? "digraph" : "graph";
223 const char* edge_operator = Directed ? " -> " : " -- ";
224
225 std::ostringstream out;
226 out << graph_keyword << " " << detail::graph_id(options.graph_name) << " {\n";
227 detail::DotAttrList graph_attrs;
228 if (options.layout.has_value()) {
229 detail::append_dot_attr(graph_attrs, "layout", detail::dot_layout_name(*options.layout));
230 }
231 for (const auto& [key, value] : options.graph_attrs) {
232 detail::append_dot_attr(graph_attrs, key, value);
233 }
234 for (const auto& [key, value] : graph_attrs) {
235 out << " " << detail::graph_id(key) << "=" << detail::attr_value(value) << ";\n";
236 }
237
238 for (const auto& node : graph.nodes()) {
239 const std::string node_text = detail::to_string(node);
240 out << " " << detail::quote_dot_string(node_text);
241 std::string attrs;
242 if (options.show_node_labels) {
243 attrs += "label=" + detail::quote_dot_string(node_text);
244 }
245 if (options.show_user_attrs) {
246 for (const auto& [key, value] : graph.node_attrs(node)) {
247 if (options.show_node_labels && key == "label") {
248 continue;
249 }
250 if (!attrs.empty()) {
251 attrs += " ";
252 }
253 attrs += detail::graph_id(key) + "=" + detail::attr_value(detail::any_attr_value(value));
254 }
255 }
256 if (!attrs.empty()) {
257 out << " [" << attrs << "]";
258 }
259 out << ";\n";
260 }
261
262 for (const auto edge_id : graph.edge_ids()) {
263 const auto [source, target] = graph.get_edge_endpoints(edge_id);
264 out << " " << detail::quote_dot_string(detail::to_string(source))
265 << edge_operator
266 << detail::quote_dot_string(detail::to_string(target));
267
268 detail::DotAttrList attrs;
269 if constexpr (Weighted) {
270 if (options.show_weights) {
271 const std::string weight = detail::to_string(graph.get_edge_weight(edge_id));
272 detail::append_dot_attr(attrs, "weight", weight);
273 if (options.show_edge_labels) {
274 detail::append_dot_attr(attrs, "label", weight);
275 }
276 }
277 }
278
279 if (options.show_edge_ids) {
280 detail::append_dot_attr(attrs, "edge_id", std::to_string(edge_id));
281 if constexpr (!Weighted) {
282 if (options.show_edge_labels) {
283 detail::append_dot_attr(attrs, "label", "edge_id=" + std::to_string(edge_id));
284 }
285 }
286 }
287
288 if (options.show_user_attrs) {
289 for (const auto& [key, value] : graph.edge_attrs(edge_id)) {
290 detail::append_dot_attr(attrs, key, detail::any_attr_value(value));
291 }
292 }
293
294 if (!attrs.empty()) {
295 out << " [" << detail::format_dot_attrs(attrs) << "]";
296 }
297 out << ";\n";
298 }
299
300 out << "}\n";
301 return out.str();
302}
303
305template <
306 typename NodeID,
307 typename EdgeWeight,
308 bool Directed,
309 bool Multi,
310 bool Weighted,
311 typename OutEdgeSelector,
312 typename VertexSelector
313>
316 const std::filesystem::path& output_path,
317 const DotOptions& options = {}
318) {
319 std::ofstream out(output_path);
320 if (!out) {
321 throw std::runtime_error("DOT export failed: could not open output file.");
322 }
323 out << to_dot(graph, options);
324}
325
326} // namespace nxpp::viz
Graph wrapper around Boost Graph Library with Python-inspired helpers.
Definition graph.hpp:288
std::pair< NodeID, NodeID > get_edge_endpoints(std::size_t edge_id) const
Returns the endpoints of an edge identified by its wrapper-managed ID.
Definition multigraph.hpp:40
DotLayout
Graphviz layout engines commonly used to render DOT output.
Definition dot.hpp:27
void write_dot(const Graph< NodeID, EdgeWeight, Directed, Multi, Weighted, OutEdgeSelector, VertexSelector > &graph, const std::filesystem::path &output_path, const DotOptions &options={})
Writes Graphviz DOT output to a filesystem path.
Definition dot.hpp:314
std::string to_dot(const Graph< NodeID, EdgeWeight, Directed, Multi, Weighted, OutEdgeSelector, VertexSelector > &graph, const DotOptions &options={})
Serializes a graph to Graphviz DOT.
Definition dot.hpp:218
Controls the lightweight DOT export surface.
Definition dot.hpp:36