require 'nodepile/base_structs.rb' require 'ruby-graphviz' module Nodepile # This class converts a set of nodes and edges into a visualization rendered # by the graphviz tool. # # Note: At present this is a wrapper around the ruby-graphviz gem as interface # to the graphviz software tool. In order for this class to work # the graphviz software package must be installed on the system. At some # point I'm probably going to want to pull out that dependency because # I'm a classic NIH bigot. But I always think about Luke and his lightsaber. class GraphVisualizer def initialize @graph = nil end def inspect = "#<#{self.class}:0x#{object_id}" NODE_ATTR_MAPS = { '_shape' => :shape, '_color' => :color, '_fillcolor' => Proc.new{|obj,s| obj[:fillcolor] = s; obj[:style] ||= 'filled'}, '_fontcolor' => :fontcolor, '_label' => :label, } EDGE_ATTR_MAPS = { '_color' => :color, '_fontcolor' => :fontcolor, '_label' => :label, } # Given edge and node packets, # # @param node_packet_enum [Enumerable] all nodes # @param edge_packet_enum [Enumerable] all edges # @param configs [#[]] may be interrogated for config info # such as the preferred rendering engine # @return [void] no meaningful return value def load(node_record_enum, edge_record_enum,configs: Hash.new) @graph = GraphViz.new(:G) @graph.type = configs[:directionality] if configs[:directionality] nodes = Hash.new # temporary cache node_record_enum.each{|nr| n = nodes[nr['@key']] = @graph.add_nodes(nr['@key']) self.class._apply_attrs(n,nr,NODE_ATTR_MAPS) } edge_record_enum.each{|er| node_pair = er['@key'].map{|k| nodes[k] } e = @graph.add_edges(*node_pair) self.class._apply_attrs(e,er,EDGE_ATTR_MAPS) } end # Generate a file based on the load specified # # @param fpath [String] filepath location at which to create the file. # Filename to create/overwrite with the generated visualization. # @param file_format [nil,:png,:gif,:svg,:dot] Note that the desired output will be # deduced from the fpath extension if it exactly matches one # of the valid arguments here (e.g. "xyz.png"). # @param configs [#[]] may be interrogated for config info # such as the preferred rendering engine # # @return [String] returns the path of the file created def emit_file(fpath,configs: nil ,file_format: nil) configs ||= Hash.new extension = /.*\.(png|svg|dot)$/.match(fpath)&.[](1) fmt = file_format || extension&.to_sym raise "Output file format is unspecified and can't be deduced from file extension" unless fmt oargs = Hash.new oargs[fmt] = fpath oargs[:use] = configs[:layout_engine] if configs[:layout_engine] @graph.output(oargs) return fpath end #emit file private # Convert field values into the rendering attributes and set them on the # target objet. # @param target [GraphViz::Node, GraphViz::Edge] # @return [void] def self._apply_attrs(target,fieldset, attr_map) fieldset.each_filled_pair{|k,v| next if !k.start_with?('_') || k == '~' # treat tilde like blank gvkey = attr_map[k] case gvkey when nil #no-op when Proc gvkey.call(target,v) else target[gvkey] = v if gvkey # naive assignment (hope the value is legit) end } return nil end end #class GraphVisualizer end #module Nodepile