lib/rails_erd/diagram/graphviz.rb in rails-erd-0.3.0 vs lib/rails_erd/diagram/graphviz.rb in rails-erd-0.4.0

- old
+ new

@@ -17,17 +17,14 @@ end module RailsERD class Diagram # Create Graphviz-based diagrams based on the domain model. For easy - # command line graph generation, you can use rake: + # command line graph generation, you can use: # # % rake erd # - # Please see the README.rdoc file for more details on how to use Rails ERD - # from the command line. - # # === Options # # The following options are supported: # # filename:: The file basename of the generated diagram. Defaults to +ERD+, @@ -35,11 +32,11 @@ # filetype:: The file type of the generated diagram. Defaults to +pdf+, which # is the recommended format. Other formats may render significantly # worse than a PDF file. The available formats depend on your installation # of Graphviz. # notation:: The cardinality notation to be used. Can be +:simple+ or - # +:advanced+. Refer to README.rdoc or to the examples on the project + # +:bachman+. Refer to README.rdoc or to the examples on the project # homepage for more information and examples. # orientation:: The direction of the hierarchy of entities. Either +:horizontal+ # or +:vertical+. Defaults to +horizontal+. The orientation of the # PDF that is generated depends on the amount of hierarchy # in your models. @@ -58,13 +55,11 @@ :pad => "0.4,0.4", :margin => "0,0", :concentrate => true, :labelloc => :t, :fontsize => 13, - :fontname => "Arial Bold", - :remincross => true, - :outputorder => :edgesfirst + :fontname => "Arial Bold" } # Default node attributes. NODE_ATTRIBUTES = { :shape => "Mrecord", @@ -84,98 +79,143 @@ :labelangle => 32, :labeldistance => 1.8, :fontsize => 7 } - # Define different styles to draw the cardinality of relationships. - CARDINALITY_STYLES = { - # Closed arrows for to/from many. - :simple => lambda { |relationship, options| - options[:arrowhead] = relationship.to_many? ? :normal : :none - options[:arrowtail] = relationship.many_to? ? :normal : :none - }, - - # Closed arrow for to/from many, UML ranges at each end. - :uml => lambda { |relationship, options| - options[:arrowsize] = 0.7 - options[:arrowhead] = relationship.to_many? ? :vee : :none - options[:arrowtail] = relationship.many_to? ? :vee : :none - ranges = [relationship.cardinality.destination_range, relationship.cardinality.source_range].map do |range| - if range.min == range.max - "#{range.min}" - else - "#{range.min}..#{range.max == Relationship::Cardinality::Infinity ? "∗" : range.max}" - end + module Simple + def entity_style(entity, attributes) + {}.tap do |options| + options[:fontcolor] = options[:color] = :grey60 if entity.abstract? end - options[:headlabel], options[:taillabel] = *ranges - }, + end - # Arrow for to/from many, open or closed dots for optional/mandatory. - :advanced => lambda { |relationship, options| - dst = relationship.destination_optional? ? "odot" : "dot" - src = relationship.source_optional? ? "odot" : "dot" - dst << "normal" if relationship.to_many? - src << "normal" if relationship.many_to? - options[:arrowsize] = 0.6 - options[:arrowhead], options[:arrowtail] = dst, src - } - } + def relationship_style(relationship) + {}.tap do |options| + options[:style] = :dotted if relationship.indirect? - def graph - @graph ||= GraphViz.digraph(@domain.name) do |graph| - # Set all default attributes. - GRAPH_ATTRIBUTES.each { |attribute, value| graph[attribute] = value } - NODE_ATTRIBUTES.each { |attribute, value| graph.node[attribute] = value } - EDGE_ATTRIBUTES.each { |attribute, value| graph.edge[attribute] = value } + # Closed arrows for to/from many. + options[:arrowhead] = relationship.to_many? ? "normal" : "none" + options[:arrowtail] = relationship.many_to? ? "normal" : "none" + end + end - # Switch rank direction if we're creating a vertically oriented graph. - graph[:rankdir] = :TB if vertical? - - # Title of the graph itself. - graph[:label] = "#{title}\\n\\n" if title + def specialization_style(specialization) + { :color => :grey60, :arrowtail => :onormal, :arrowhead => :none, :arrowsize => 1.2 } end end - - # Save the diagram and return the file name that was written to. - def save - graph.output(filetype => filename) - filename - rescue StandardError => e - raise "Saving diagram failed. Verify that Graphviz is installed or select filetype=dot." - end + + module Bachman + include Simple + def relationship_style(relationship) + {}.tap do |options| + options[:style] = :dotted if relationship.indirect? - protected + # Participation is "look-here". + dst = relationship.source_optional? ? "odot" : "dot" + src = relationship.destination_optional? ? "odot" : "dot" - def process_entity(entity, attributes) - graph.add_node entity.name, entity_options(entity, attributes) + # Cardinality is "look-across". + dst << "normal" if relationship.to_many? + src << "normal" if relationship.many_to? + options[:arrowsize] = 0.6 + options[:arrowhead], options[:arrowtail] = dst, src + end + end end + + module Uml + include Simple + def relationship_style(relationship) + {}.tap do |options| + options[:style] = :dotted if relationship.indirect? - def process_relationship(relationship) - graph.add_edge graph.get_node(relationship.source.name), graph.get_node(relationship.destination.name), - relationship_options(relationship) - end + options[:arrowsize] = 0.7 + options[:arrowhead] = relationship.to_many? ? "vee" : "none" + options[:arrowtail] = relationship.many_to? ? "vee" : "none" - # Returns +true+ if the layout or hierarchy of the diagram should be - # horizontally oriented. - def horizontal? - options.orientation == :horizontal + ranges = [relationship.cardinality.destination_range, relationship.cardinality.source_range].map do |range| + if range.min == range.max + "#{range.min}" + else + "#{range.min}..#{range.max == Domain::Relationship::N ? "∗" : range.max}" + end + end + options[:headlabel], options[:taillabel] = *ranges + end + end end + + attr_accessor :graph - # Returns +true+ if the layout or hierarchy of the diagram should be - # vertically oriented. - def vertical? - !horizontal? + setup do + self.graph = GraphViz.digraph(domain.name) + + # Set all default attributes. + GRAPH_ATTRIBUTES.each { |attribute, value| graph[attribute] = value } + NODE_ATTRIBUTES.each { |attribute, value| graph.node[attribute] = value } + EDGE_ATTRIBUTES.each { |attribute, value| graph.edge[attribute] = value } + + # Switch rank direction if we're creating a vertically oriented graph. + graph[:rankdir] = :TB if options.orientation == :vertical + + # Title of the graph itself. + graph[:label] = "#{title}\\n\\n" if title + + # Setup notation options. + extend self.class.const_get(options.notation.to_s.capitalize.to_sym) end + + save do + raise "Saving diagram failed. Output directory '#{File.dirname(filename)}' does not exist." unless File.directory?(File.dirname(filename)) + begin + graph.output(filetype => filename) + filename + rescue StandardError => e + raise "Saving diagram failed. Verify that Graphviz is installed or select filetype=dot." + end + end + each_entity do |entity, attributes| + draw_node entity.name, entity_options(entity, attributes) + end + + each_specialization do |specialization| + from, to = specialization.generalized, specialization.specialized + draw_edge from.name, to.name, specialization_options(specialization) + end + + each_relationship do |relationship| + from, to = relationship.source, relationship.destination + unless draw_edge from.name, to.name, relationship_options(relationship) + if from.children.any? + from.children.each do |child| + draw_edge child.name, to.name, relationship_options(relationship) + end + end + end + end + private + def node_exists?(name) + !!graph.get_node(name) + end + + def draw_node(name, options) + graph.add_node name, options + end + + def draw_edge(from, to, options) + graph.add_edge graph.get_node(from), graph.get_node(to), options if node_exists?(from) and node_exists?(to) + end + # Returns the title to be used for the graph. def title case options.title when false then nil - when true then - if @domain.name then "#{@domain.name} domain model" else "Domain model" end + when true + if domain.name then "#{domain.name} domain model" else "Domain model" end else options.title end end # Returns the file name that will be used when saving the diagram. @@ -186,34 +226,25 @@ # Returns the default file extension to be used when saving the diagram. def filetype if options.filetype.to_sym == :dot then :none else options.filetype.to_sym end end - # Returns an options hash based on the given entity and its attributes. def entity_options(entity, attributes) - { :label => "<#{NODE_LABEL_TEMPLATE.result(binding)}>" } + entity_style(entity, attributes).merge :label => "<#{NODE_LABEL_TEMPLATE.result(binding)}>" end - # Returns an options hash def relationship_options(relationship) - relationship_style_options(relationship).tap do |opts| + relationship_style(relationship).tap do |options| # Edges with a higher weight are optimised to be shorter and straighter. - opts[:weight] = relationship.strength + options[:weight] = relationship.strength # Indirect relationships should not influence node ranks. - opts[:constraint] = false if relationship.indirect? + options[:constraint] = false if relationship.indirect? end end - - # Returns an options hash that defines the (cardinality) style for the - # relationship. - def relationship_style_options(relationship) - {}.tap do |opts| - opts[:style] = :dotted if relationship.indirect? - - # Let cardinality style callbacks draw arrow heads and tails. - CARDINALITY_STYLES[options.notation][relationship, opts] - end + + def specialization_options(specialization) + specialization_style(specialization) end end end -end \ No newline at end of file +end