class ChaosDetector::Graphing::DirectedGraphs

Constants

BOLD_HTML
BR
CLR_BLACK
CLR_BLUE
CLR_BRIGHTGREEN
CLR_CYAN
CLR_DARKGREEN
CLR_DARKRED
CLR_GREY
CLR_NICEGREY
CLR_ORANGE
CLR_PALEGREEN
CLR_PINK
CLR_PURPLE
CLR_SLATE
CLR_WHITE
EDGE_ATTRS
EDGE_BASELINE
EDGE_MIN
GRAPH_ATTRS
LF
NODE_ATTRS
PRE_STATUS_MSG

Status messages:

STUB_NODE_ATTRS
SUBDOMAIN_ATTRS
TBL_CELL_HTML
TBL_HTML
TBL_ROW_HTML

Attributes

cluster_node_hash[R]
edges[R]
node_hash[R]
render_folder[R]
rendered_path[R]
root_graph[R]

Public Class Methods

new(render_folder: nil) click to toggle source

TODO: integrate options as needed:

# File lib/chaos_detector/graphing/directed_graphs.rb, line 133
def initialize(render_folder: nil)
  @root_graph = nil
  @node_hash = {}
  @cluster_node_hash = {}
  @edges = Set.new
  @render_folder = render_folder || 'render'
end

Public Instance Methods

add_edges(edges, calc_weight:true, node_safe:true) click to toggle source
# File lib/chaos_detector/graphing/directed_graphs.rb, line 283
def add_edges(edges, calc_weight:true, node_safe:true)
  assert_graph_state

  # @node_hash.each do |k, v|
  #   log("NODE_HASH: Has value for #{ChaosUtils.decorate(k)} => #{ChaosUtils.decorate(v)}")
  # end

  weight_max  = edges.map(&:weight).max

  edges.each do |edge|
    src_clust, src = find_graph_node(edge.src_node)
    dep_clust, dep = find_graph_node(edge.dep_node)
    next unless !node_safe || (src && dep)

    @edges << [src, dep]          
    edge_attrs = build_edge_attrs(edge, calc_weight: calc_weight, max_weight: weight_max, src_clust: src_clust, dep_clust: dep_clust)
    
    @root_graph.add_edges(
      node_key(edge.src_node, cluster: src_clust ? :stub : false),
      node_key(edge.dep_node, cluster: dep_clust ? :stub : false),
      edge_attrs
    ) # , {label: e.reduce_sum, penwidth: weight})
  end
end
add_node_to_graph(node, graph: nil, as_cluster: false, metrics_table:false) click to toggle source
# File lib/chaos_detector/graphing/directed_graphs.rb, line 235
def add_node_to_graph(node, graph: nil, as_cluster: false, metrics_table:false)
  assert_graph_state
  raise 'node is required' unless node

  parent_graph = graph || @root_graph
  key = node.to_k

  attrs = { label: node_label(node, metrics_table: metrics_table) }

  if as_cluster
    # tab good shape
    subgraph_name = "cluster_#{key}"
    attrs.merge!(SUBDOMAIN_ATTRS)
    # attrs = {}.merge(SUBDOMAIN_ ATTRS)
    @cluster_node_hash[key] = parent_graph.add_graph(subgraph_name, attrs)

    @cluster_node_hash[key].add_nodes(node_key(node, cluster: :stub), STUB_NODE_ATTRS)

    # @cluster_node_hash[key].attrs(attrs)
    # puts ("attrs: #{key}: #{attrs}  / #{@cluster_node_hash.length}")
  else
    attrs = attrs.merge!(NODE_ATTRS)
    @node_hash[key] = parent_graph.add_nodes(key, attrs)
  end
end
add_node_to_parent(node, parent_node:, as_cluster: false, metrics_table:false) click to toggle source

Add node to given parent_node, assuming parent_node is a subgraph

# File lib/chaos_detector/graphing/directed_graphs.rb, line 220
def add_node_to_parent(node, parent_node:, as_cluster: false, metrics_table:false)
  assert_graph_state
  raise 'node is required' unless node

  parent_graph = if parent_node
    _clust, p_graph = find_graph_node(parent_node)
    raise "Couldn't find parent node: #{parent_node}" unless p_graph
    p_graph
  else
    @root_graph
  end

  add_node_to_graph(node, graph: parent_graph, as_cluster: as_cluster, metrics_table: metrics_table)
end
append_nodes(nodes, as_cluster: false, metrics_table: false) { |node| ... } click to toggle source
# File lib/chaos_detector/graphing/directed_graphs.rb, line 261
def append_nodes(nodes, as_cluster: false, metrics_table: false)
  assert_graph_state
  return unless nodes
  # raise 'node is required' unless nodes

  nodes.each do |node|
    parent_node = block_given? ? yield(node) : nil
    # puts "gotit #{parent_node}" if parent_node
    add_node_to_parent(node, parent_node: parent_node, as_cluster: as_cluster, metrics_table: metrics_table)
  end
end
assert_graph_state() click to toggle source
# File lib/chaos_detector/graphing/directed_graphs.rb, line 215
def assert_graph_state
  raise '@root_graph is not set yet.  Call create_directed_graph.' unless @root_graph
end
create_directed_graph(title, graph_attrs: nil) click to toggle source
# File lib/chaos_detector/graphing/directed_graphs.rb, line 141
def create_directed_graph(title, graph_attrs: nil)
  @title = title

  @node_hash.clear
  @cluster_node_hash.clear
  @cluster_node_hash.clear

  lbl = title_html(@title, subtitle: graph_attrs&.dig(:subtitle))

  attrs = {
    label: "<#{lbl}>",
    **GRAPH_ATTRS,
  }
  attrs.merge(graph_attrs) if graph_attrs&.any?
  attrs.delete(:subtitle)

  @root_graph = GraphViz.digraph(:G, attrs)
end
html_tbl_from(hash:) { |k, v| ... } click to toggle source
# File lib/chaos_detector/graphing/directed_graphs.rb, line 198
def html_tbl_from(hash:)
  trs = hash.map.with_index do |h, n|
    k, v = h
    key_content, val_content = yield(k, v) if block_given?
    key_td = TBL_CELL_HTML % (key_content || k)
    val_td = TBL_CELL_HTML % (val_content || v)
    td_html = [key_td, val_td].join
    html = format(TBL_ROW_HTML, {
      color: n.even? ? 'blue' : 'white',
      cells: td_html.strip
    })
    html.strip
  end

  TBL_HTML % trs.join().strip
end
in_font(str, font_size:12) click to toggle source
# File lib/chaos_detector/graphing/directed_graphs.rb, line 194
def in_font(str, font_size:12)
  "<FONT POINT-SIZE='#{font_size}'>#{str}</FONT>"
end
node_key(node, cluster: false) click to toggle source
# File lib/chaos_detector/graphing/directed_graphs.rb, line 273
def node_key(node, cluster: false)
  if cluster==:stub
    "cluster_stub_#{node.to_k}"
  elsif !!cluster
    "cluster_#{node.to_k}"
  else
    node.to_k
  end
end
node_label(node, metrics_table: false) click to toggle source
# File lib/chaos_detector/graphing/directed_graphs.rb, line 160
def node_label(node, metrics_table: false)
  if metrics_table
    tbl_hash = {title: node.title,  subtitle: node.subtitle,  **node.graph_props}
    html = html_tbl_from(hash: tbl_hash) do |k, v|
      if k==:title
        [in_font(k, font_size: 24), in_font(v, font_size: 16)]#[BOLD_HTML % k, BOLD_HTML % v]
      else
        [k, v]
      end
    end
  else
    html = title_html(node.title, subtitle: node.subtitle)
  end
  html.strip!
  # puts '_' * 50
  # puts "html: #{html}"
  '<%s>' % html
end
render_graph() click to toggle source
# File lib/chaos_detector/graphing/directed_graphs.rb, line 308
def render_graph
  assert_graph_state

  filename = "#{@title}.png"
  @rendered_path = File.join(@render_folder, filename).to_s

  log("Rendering graph to to #{@rendered_path}")
  ChaosDetector::Utils::FSUtil.ensure_paths_to_file(@rendered_path)
  @root_graph.output(png: @rendered_path)
  self
end
title_html(title, subtitle:nil, font_size:24, subtitle_fontsize:nil) click to toggle source

HTML Label with subtitle:

# File lib/chaos_detector/graphing/directed_graphs.rb, line 180
def title_html(title, subtitle:nil, font_size:24, subtitle_fontsize:nil)
  lbl_buf = [in_font(title, font_size: font_size)]

  sub_fontsize = subtitle_fontsize || 3 * font_size / 4
  if ChaosUtils.aught?(subtitle)
    lbl_buf << in_font(subtitle, font_size: sub_fontsize)
  end

  # Fake out some padding:
  lbl_buf << in_font(' ', font_size: sub_fontsize)

  lbl_buf.join(BR)
end

Private Instance Methods

build_edge_attrs(edge, calc_weight: true, max_weight: nil, src_clust: nil, dep_clust: nil) click to toggle source
# File lib/chaos_detector/graphing/directed_graphs.rb, line 322
def build_edge_attrs(edge, calc_weight: true, max_weight: nil, src_clust: nil, dep_clust: nil)        
  edge_attrs = EDGE_ATTRS.dup
  
  # Edge attaches to cluster if possible:
  edge_attrs[:ltail] = node_key(edge.src_node, cluster: true) if src_clust
  edge_attrs[:lhead] = node_key(edge.dep_node, cluster: true) if dep_clust

  # Proportional edge weight:
  if calc_weight && max_weight
    edge_attrs.merge!(
      label: edge.weight,
      penwidth: edge_weight(edge.weight / max_weight)
    )          
  end
 
  # Intra-domain:
  if edge.src_node.domain_name == edge.dep_node.domain_name
    edge_attrs.merge!(
      style: 'dotted',
      color: CLR_ORANGE,
      constraint: 'true',
    )
  end
  
  # Props for edge_type:
  edge_attrs.merge!(       
    case edge.edge_type
      when :superclass
        {
          arrowhead: 'empty',
          arrowsize: 1.0,
          color: CLR_BLUE
        }
      when :association
        {
          arrowhead: 'diamond',
          arrowsize: 1.0,
          color: CLR_ORANGE
        }
      when :class_association
        {
          arrowhead: 'diamond',
          arrowsize: 1.0,
          color: CLR_PINK
        }
      else
        {
          arrowhead: 'open',
          arrowsize: 1.0
        }
    end
  )
end
edge_weight(n, edge_min: EDGE_MIN, edge_baseline: EDGE_BASELINE) click to toggle source
# File lib/chaos_detector/graphing/directed_graphs.rb, line 387
def edge_weight(n, edge_min: EDGE_MIN, edge_baseline: EDGE_BASELINE)
  edge_min + n * edge_baseline
end
find_graph_node(node) click to toggle source
# File lib/chaos_detector/graphing/directed_graphs.rb, line 376
def find_graph_node(node)
  assert_graph_state
  # log("NODE_HASH: LOOKING UP #{ChaosUtils.decorate(node)}")
  cnode = @cluster_node_hash[node.to_k]
  if cnode
    [true, cnode]
  else
    [false, @node_hash[node.to_k]]
  end
end
log(msg, **opts) click to toggle source
# File lib/chaos_detector/graphing/directed_graphs.rb, line 391
def log(msg, **opts)
  ChaosUtils.log_msg(msg, subject: 'DGraphDiagram', **opts)
end