Skip to content

Instantly share code, notes, and snippets.

@vizvamitra
Last active January 26, 2016 11:44
Show Gist options
  • Save vizvamitra/c7afa0e7fd09f5023825 to your computer and use it in GitHub Desktop.
Save vizvamitra/c7afa0e7fd09f5023825 to your computer and use it in GitHub Desktop.
# converts directed graph to dot string
#
# Usage:
#
# my_digraph = {node1: [:node2, :node3], node2: [:node3], node3: [:node4], node4: []}
# formatter = DigraphDotFormatter.new(my_digraph)
#
# formatter.set_node_attributes(:node1, label: 'Start', fillcolor: '#0000ff')
# formatter.set_node_attributes(:node4, label: 'End', fillcolor: '#00ff00')
# formatter.set_edge_attributes(:node3, :node4, label: 'Last move')
#
# formatter.to_dot # =>
# digraph my_graph {
# /***** GLOBAL SETTINGS *****/
# graph [rotate=0, rankdir="LR"]
# node [color="white", style="filled",
# shape="box", fillcolor="black",
# fontcolor="white", fontsize="14"]
# edge [color="#666666", arrowhead="open",
# fontsize="11"]
# /***** Nodes *****/
# _48d5fa06c371 [label="Start", fillcolor="#0000ff"]
# _302c2aa8b889
# _6b8a0b4095d3
# _4d2114173780 [label="End", fillcolor="#00ff00"]
# /***** Edges *****/
# _48d5fa06c371 -> _302c2aa8b889
# _48d5fa06c371 -> _6b8a0b4095d3
# _302c2aa8b889 -> _6b8a0b4095d3
# _6b8a0b4095d3 -> _4d2114173780 [label="Last move"]
# }
class DigraphDotFormatter
class NodeNotFound < StandardError; end
# DEFAULT SETTINGS
NODE_COLOR = 'white'
NODE_FILLCOLOR = 'black'
NODE_STYLE = 'filled'
NODE_SHAPE = 'box'
NODE_FONTCOLOR = 'white'
NODE_FONTSIZE = '14'
EDGE_COLOR = '#666666'
EDGE_ARROWHED = 'open'
EDGE_FONTSIZE = '11'
attr_reader :graph, :nodes, :edges
def initialize(graph)
@graph = graph
@nodes = {}
@edges = {}
build_nodes
build_edges
end
def set_node_attributes(graph_node, attrs)
node = nodes[graph_node]
attrs.each{|attr_name, value| node.send("#{attr_name}=", value)}
end
def set_edge_attributes(from, to, attrs)
edge = edges[{from => to}]
attrs.each{|attr_name, value| edge.send("#{attr_name}=", value)}
end
def to_dot
<<-DOT
digraph quest {
/***** GLOBAL SETTINGS *****/
graph [rotate=0, rankdir="LR"]
node [color="#{NODE_COLOR}", style="#{NODE_STYLE}",
shape="#{NODE_SHAPE}", fillcolor="#{NODE_FILLCOLOR}",
fontcolor="#{NODE_FONTCOLOR}", fontsize="#{NODE_FONTSIZE}"]
edge [color="#{EDGE_COLOR}", arrowhead="#{EDGE_ARROWHED}",
fontsize="#{EDGE_FONTSIZE}"]
/***** Nodes *****/
#{nodes_string}
/***** Edges *****/
#{edges_string}
}
DOT
end
private
def build_nodes
graph.keys.map{|node| build_node(node)}
end
def build_node(node)
nodes[node] = Node.new
end
def build_edges
graph.map do |node_from, nodes_to|
nodes_to.each{|node_to| build_edge(node_from, node_to)}
end
end
def build_edge(node_from, node_to)
raise NodeNotFound if !nodes[node_from] || !nodes[node_to]
edges[{node_from => node_to}] = Edge.new(nodes[node_from], nodes[node_to])
end
def nodes_string
nodes.values.map(&:to_s).join("\n")
end
def edges_string
edges.values.map(&:to_s).join("\n")
end
module OptionsString
private
def options_string
return '' unless values.any?
"[#{to_h.map{|key, value| value ? "#{key}=\"#{value}\"" : nil}.compact.join(', ')}]"
end
end
Node = Struct.new(:label, :color, :fillcolor, :style, :shape, :fontcolor, :fontsize) do
include OptionsString
attr_reader :id
def initialize(label: nil, color: nil, fillcolor: nil, style: nil, shape: nil, fontcolor: nil, fontsize: nil)
@id = "_#{SecureRandom.hex(6)}"
super(label, color, fillcolor, style, shape, fontcolor, fontsize)
end
def to_s
"#{id} #{options_string}"
end
end
Edge = Struct.new(:label, :color, :arrowhead, :fontsize) do
include OptionsString
attr_reader :from, :to
def initialize(from, to, label: nil, color: nil, arrowhead: nil, fontsize: nil)
@from = from
@to = to
super(label, color, arrowhead, fontsize)
end
def to_s
"#{from.id} -> #{to.id} #{options_string}"
end
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment