Module: Rigor::ModuleGraph::Dot
- Defined in:
- lib/rigor/module_graph/dot.rb
Overview
Renders an array of Edges as a Graphviz DOT document.
Style decisions (per docs/plan.md “グラフモデル”):
-
rankdir=LR for readability of inheritance towers
-
inherits: thick solid
-
include: solid
-
prepend: solid, distinct color
-
extend: dashed
-
const_ref: faded dotted
When ‘collapse:` is given, every node whose fully-qualified name sits under one of the listed prefixes is wrapped in a `subgraph cluster_<prefix>` block, and the prefix is stripped from the visible label. Edges across clusters render normally; Graphviz routes them between the cluster boundaries.
Constant Summary collapse
- KIND_STYLE =
{ "inherits" => 'color="#0f172a", penwidth=2.0', "include" => 'color="#1d4ed8"', "prepend" => 'color="#9333ea"', "extend" => 'color="#0f766e", style="dashed"', "const_ref" => 'color="#94a3b8", style="dotted"' }.freeze
- CONFIDENCE_STYLE =
{ "unresolved" => 'style="dashed", color="#94a3b8"' }.freeze
- HEADER =
<<~DOT digraph ruby_modules { rankdir=LR; graph [compound=true, overlap=false, splines=true]; node [shape=box, style="rounded,filled", fillcolor="#f8fafc", color="#94a3b8", fontname="Helvetica"]; edge [color="#64748b", arrowsize=0.7, fontname="Helvetica"]; DOT
Class Method Summary collapse
-
.build_groups(nodes, collapse, groups) ⇒ Object
Build the cluster partition.
-
.cluster_id(prefix) ⇒ Object
Cluster identifiers in DOT must match ‘[A-Za-z_]*` — package names like `packages/billing` would otherwise break Graphviz’s parser even inside quotes.
- .collect_nodes(edges) ⇒ Object
- .dedup(edges) ⇒ Object
- .group_by_prefix(nodes, collapse) ⇒ Object
- .quote(name) ⇒ Object
- .render(edges, collapse: [], groups: nil) ⇒ Object
- .render_cluster(label, members, use_namespace_prefix: true) ⇒ Object
- .render_edge(edge) ⇒ Object
Class Method Details
.build_groups(nodes, collapse, groups) ⇒ Object
Build the cluster partition. When groups is given we use it verbatim; otherwise fall back to prefix-matching against collapse (the legacy namespace-collapse path).
89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 |
# File 'lib/rigor/module_graph/dot.rb', line 89 def build_groups(nodes, collapse, groups) if groups && !groups.empty? clusters = Hash.new { |h, k| h[k] = [] } ungrouped = [] nodes.each do |name| if (label = groups[name]) clusters[label] << name else ungrouped << name end end [clusters, ungrouped] else group_by_prefix(nodes, collapse) end end |
.cluster_id(prefix) ⇒ Object
Cluster identifiers in DOT must match ‘[A-Za-z_]*` — package names like `packages/billing` would otherwise break Graphviz’s parser even inside quotes. Squash every non-id character to ‘_` so the prefix `cluster_` still triggers Graphviz’s cluster handling.
142 143 144 |
# File 'lib/rigor/module_graph/dot.rb', line 142 def cluster_id(prefix) prefix.gsub(/[^A-Za-z0-9_]+/, "_") end |
.collect_nodes(edges) ⇒ Object
81 82 83 84 |
# File 'lib/rigor/module_graph/dot.rb', line 81 def collect_nodes(edges) names = edges.flat_map { |edge| [edge.from, edge.to] } names.uniq.sort end |
.dedup(edges) ⇒ Object
70 71 72 73 74 75 76 77 78 79 |
# File 'lib/rigor/module_graph/dot.rb', line 70 def dedup(edges) seen = {} edges.each_with_object([]) do |edge, acc| key = edge.dedup_key next if seen[key] seen[key] = true acc << edge end end |
.group_by_prefix(nodes, collapse) ⇒ Object
106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 |
# File 'lib/rigor/module_graph/dot.rb', line 106 def group_by_prefix(nodes, collapse) prefixes = Array(collapse).map(&:to_s).reject(&:empty?) return [{}, nodes] if prefixes.empty? sorted = prefixes.sort_by { |p| -p.length } clusters = Hash.new { |h, k| h[k] = [] } ungrouped = [] nodes.each do |name| match = sorted.find { |p| name.start_with?(p + "::") } if match clusters[match] << name else ungrouped << name end end [clusters, ungrouped] end |
.quote(name) ⇒ Object
157 158 159 160 161 |
# File 'lib/rigor/module_graph/dot.rb', line 157 def quote(name) # DOT identifiers that contain `::` or quotes must be # double-quoted; escape embedded double quotes. '"' + name.gsub('"', '\"') + '"' end |
.render(edges, collapse: [], groups: nil) ⇒ Object
51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 |
# File 'lib/rigor/module_graph/dot.rb', line 51 def render(edges, collapse: [], groups: nil) edges = dedup(edges) nodes = collect_nodes(edges) clusters, ungrouped = build_groups(nodes, collapse, groups) out = +HEADER clusters.each do |label, members| out << render_cluster(label, members, use_namespace_prefix: groups.nil?) end ungrouped.each do |name| out << " #{quote(name)};\n" end out << "\n" unless nodes.empty? edges.each do |edge| out << render_edge(edge) end out << "}\n" end |
.render_cluster(label, members, use_namespace_prefix: true) ⇒ Object
124 125 126 127 128 129 130 131 132 133 134 135 |
# File 'lib/rigor/module_graph/dot.rb', line 124 def render_cluster(label, members, use_namespace_prefix: true) out = +" subgraph #{quote("cluster_" + cluster_id(label))} {\n" out << " label=#{quote(label)};\n" out << " style=\"rounded,filled\";\n" out << " color=\"#cbd5e1\";\n" out << " fillcolor=\"#f1f5f9\";\n" members.each do |name| short = use_namespace_prefix ? name.sub(/\A#{Regexp.escape(label)}::/, "") : name out << " #{quote(name)} [label=#{quote(short)}];\n" end out << " }\n" end |
.render_edge(edge) ⇒ Object
146 147 148 149 150 151 152 153 154 155 |
# File 'lib/rigor/module_graph/dot.rb', line 146 def render_edge(edge) attrs = +"label=\"#{edge.kind}\"" if (style = KIND_STYLE[edge.kind]) attrs << ", " << style end if (style = CONFIDENCE_STYLE[edge.confidence]) attrs << ", " << style end " #{quote(edge.from)} -> #{quote(edge.to)} [#{attrs}];\n" end |