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