lib/rails_erd/diagram/graphviz.rb in rails-erd-0.2.0 vs lib/rails_erd/diagram/graphviz.rb in rails-erd-0.3.0
- old
+ new
@@ -1,5 +1,6 @@
+# encoding: utf-8
require "rails_erd/diagram"
require "graphviz"
require "erb"
# Fix bad RegEx test in Ruby-Graphviz.
@@ -22,64 +23,126 @@
#
# % 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+,
+ # or any other extension based on the file type.
+ # 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
+ # 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.
+ # title:: The title to add at the top of the diagram. Defaults to
+ # <tt>"YourApplication domain model"</tt>.
class Graphviz < Diagram
NODE_LABEL_TEMPLATE = ERB.new(File.read(File.expand_path("templates/node.erb", File.dirname(__FILE__))), nil, "<>") # @private :nodoc:
NODE_WIDTH = 130 # @private :nodoc:
# Default graph attributes.
GRAPH_ATTRIBUTES = {
:rankdir => :LR,
:ranksep => 0.5,
- :nodesep => 0.35,
- :margin => "0.4,0.4",
+ :nodesep => 0.4,
+ :pad => "0.4,0.4",
+ :margin => "0,0",
:concentrate => true,
:labelloc => :t,
:fontsize => 13,
:fontname => "Arial Bold",
:remincross => true,
- :outputorder => :edgesfirst }
+ :outputorder => :edgesfirst
+ }
# Default node attributes.
NODE_ATTRIBUTES = {
:shape => "Mrecord",
:fontsize => 10,
:fontname => "Arial",
:margin => "0.07,0.05",
- :penwidth => 0.8 }
+ :penwidth => 1.0
+ }
# Default edge attributes.
EDGE_ATTRIBUTES = {
:fontname => "Arial",
:fontsize => 8,
:dir => :both,
- :arrowsize => 0.7,
- :penwidth => 0.8 }
+ :arrowsize => 0.9,
+ :penwidth => 1.0,
+ :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
+ end
+ options[:headlabel], options[:taillabel] = *ranges
+ },
+
+ # 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 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 }
- # Switch rank direction if we're told to create a vertically
- # oriented graph.
+ # Switch rank direction if we're creating a vertically oriented graph.
graph[:rankdir] = :TB if vertical?
# Title of the graph itself.
- graph[:label] = "#{@domain.name} domain model\\n\\n"
+ graph[:label] = "#{title}\\n\\n" if title
end
end
# Save the diagram and return the file name that was written to.
def save
- graph.output(options.file_type.to_sym => file_name)
- file_name
+ graph.output(filetype => filename)
+ filename
+ rescue StandardError => e
+ raise "Saving diagram failed. Verify that Graphviz is installed or select filetype=dot."
end
protected
def process_entity(entity, attributes)
@@ -88,32 +151,68 @@
def process_relationship(relationship)
graph.add_edge graph.get_node(relationship.source.name), graph.get_node(relationship.destination.name),
relationship_options(relationship)
end
-
+
+ # Returns +true+ if the layout or hierarchy of the diagram should be
+ # horizontally oriented.
+ def horizontal?
+ options.orientation == :horizontal
+ end
+
+ # Returns +true+ if the layout or hierarchy of the diagram should be
+ # vertically oriented.
+ def vertical?
+ !horizontal?
+ end
+
private
+ # 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
+ else options.title
+ end
+ end
+
# Returns the file name that will be used when saving the diagram.
- def file_name
- "ERD.#{options.file_type}"
+ def filename
+ "#{options.filename}.#{options.filetype}"
end
+
+ # 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)}>" }
end
# Returns an options hash
def relationship_options(relationship)
- {}.tap do |options|
- options[:arrowhead] = relationship.cardinality.one_to_one? ? :dot : :normal
- options[:arrowtail] = relationship.cardinality.many_to_many? ? :normal : :dot
- options[:weight] = relationship.strength
- if relationship.indirect?
- options[:style] = :dotted
- options[:constraint] = false
- end
+ relationship_style_options(relationship).tap do |opts|
+ # Edges with a higher weight are optimised to be shorter and straighter.
+ opts[:weight] = relationship.strength
+
+ # Indirect relationships should not influence node ranks.
+ opts[: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
end
end
end
end
\ No newline at end of file