Class: RSyntaxTree::BaseGraph

Inherits:
Object
  • Object
show all
Defined in:
lib/rsyntaxtree/base_graph.rb

Direct Known Subclasses

LsifGraph, SVGGraph

Instance Method Summary collapse

Constructor Details

#initialize(element_list, params, global) ⇒ BaseGraph

Returns a new instance of BaseGraph.



14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
# File 'lib/rsyntaxtree/base_graph.rb', line 14

def initialize(element_list, params, global)
  @global = global
  @element_list = element_list
  @symmetrize = params[:symmetrize]
  @direction = params[:direction] || "ttb"

  case params[:color]
  # Okabe-Ito Color
  when "modern"
    @col_node = "#0072B2" # blue
    @col_leaf = "#009E73" # bluishgreen
    @col_path = "#CC79A7" # reddishpurple
    @col_extra = "#CC79A7" # orange
    @col_emph = "#D55E00" # vermillion
    # "#000000" black
    # "#56B4E9" skyblue
    # "#F0E442" yellow
    # "#999999" grey
  when "traditional"
    @col_node  = "blue"
    @col_leaf  = "green"
    @col_path = "purple"
    @col_extra = "purple"
    @col_emph = "red"
  else
    @col_node  = "black"
    @col_leaf  = "black"
    @col_path = "black"
    @col_extra = "black"
  end

  @col_bg   = "none"
  @col_fg   = "black"

  @col_line = if params[:hide_default_connectors] == true
                "none"
              else
                "black"
              end

  @leafstyle = params[:leafstyle]
  @fontset = params[:fontset]
  @fontsize = params[:fontsize]
end

Instance Method Details

#calculate_height(id = 1) ⇒ Object



108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
# File 'lib/rsyntaxtree/base_graph.rb', line 108

def calculate_height(id = 1)
  target = @element_list.get_id(id)
  if id == 1
    target.vertical_indent = 0
  else
    parent = @element_list.get_id(target.parent)

    vertical_indent = if !target.triangle &&
                         (@leafstyle == "nothing" || @leafstyle == "none") &&
                         ETYPE_LEAF == target.type && parent.children.size == 1
                        if @direction == "ltr"
                          # LTR: add small horizontal gap between parent and leaf
                          parent.vertical_indent + parent.content_height + @global[:height_connector_to_text]
                        else
                          parent.vertical_indent + parent.content_height
                        end
                      else
                        parent.vertical_indent + parent.content_height + @global[:height_connector]
                      end
    target.vertical_indent = vertical_indent
  end

  if target.children.empty?
    target.height = target.content_height
    target.vertical_indent + target.content_height
  else
    accum_array = []
    target.children.each do |c|
      accum_array << calculate_height(c)
    end
    target.height = accum_array.max - target.vertical_indent
    accum_array.max
  end
end

#calculate_indentObject



170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
# File 'lib/rsyntaxtree/base_graph.rb', line 170

def calculate_indent
  node_groups = @element_list.get_elements.group_by(&:parent)
  node_groups.each do |k, v|
    next if k.zero?

    parent = @element_list.get_id(k)
    if @symmetrize
      num_leaves = v.size
      partition_width = parent.width / num_leaves
      left_offset = parent.horizontal_indent + parent.content_width / 2.0 - parent.width / 2.0
      v.each do |e|
        indent = left_offset + (partition_width - e.content_width) / 2.0
        e.horizontal_indent = indent
        left_offset += partition_width
      end
    else
      left_offset = parent.horizontal_indent + parent.content_width / 2.0 - parent.width / 2.0
      v.each do |e|
        indent = left_offset + (e.width - e.content_width) / 2.0
        e.horizontal_indent = indent
        left_offset += e.width
      end
    end
  end
end

#calculate_levelObject



59
60
61
62
63
64
# File 'lib/rsyntaxtree/base_graph.rb', line 59

def calculate_level
  @element_list.get_elements.select { |e| e.type == 2 }.each do |e|
    parent = @element_list.get_id(e.parent)
    e.level = @element_list.get_id(e.parent).level + 1 if parent
  end
end

#calculate_width(id = 1) ⇒ Object



66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
# File 'lib/rsyntaxtree/base_graph.rb', line 66

def calculate_width(id = 1)
  target = @element_list.get_id(id)
  if target.children.empty?
    target.width = target.content_width + @global[:h_gap_between_nodes] * 4

    parent = @element_list.get_id(target.parent)
    while parent && parent.children.size == 1
      w = parent.content_width
      target.width = w + @global[:h_gap_between_nodes] * 4 if w > target.content_width
      parent = @element_list.get_id(parent.parent)
    end
    target.width
  else
    return target.width if target.width != 0

    accum_array = []
    target.children.each do |c|
      accum_array << calculate_width(c)
    end
    accum_width = if @symmetrize
                    accum_array.max * target.children.size
                  else
                    accum_array.sum
                  end

    if target.content_width > accum_width
      # Parent label is wider than children's total width.
      # Distribute the excess equally among children to prevent
      # child labels from overlapping when centered in their slots.
      excess = target.content_width - accum_width
      per_child = excess / target.children.size.to_f
      target.children.each do |c|
        child = @element_list.get_id(c)
        child.width += per_child
      end
      target.width = target.content_width
    else
      target.width = accum_width
    end
  end
end

#draw_connector(id = 1) ⇒ Object



202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
# File 'lib/rsyntaxtree/base_graph.rb', line 202

def draw_connector(id = 1)
  parent = @element_list.get_id(id)
  children = parent.children.map { |c| @element_list.get_id(c) }

  if children.size == 1
    child = children[0]
    case @leafstyle
    when "auto"
      if parent.triangle || child.contains_phrase
        triangle_to_parent(parent, child)
      else
        line_to_parent(parent, child)
      end
    when "bar"
      if parent.triangle
        triangle_to_parent(parent, child)
      else
        line_to_parent(parent, child)
      end
    when "nothing", "none"
      if parent.triangle
        triangle_to_parent(parent, child)
      elsif ETYPE_LEAF != child.type
        line_to_parent(parent, child)
      end
    end
  else
    children.each do |child|
      line_to_parent(parent, child)
    end
  end

  parent.children.each do |c|
    draw_connector(c)
  end
end

#draw_elementsObject



196
197
198
199
200
# File 'lib/rsyntaxtree/base_graph.rb', line 196

def draw_elements
  @element_list.get_elements.each do |element|
    draw_element(element)
  end
end

#finalize_ltrObject

Phase 2 (after layout, before drawing): swap position axes and restore original content dimensions for correct text rendering.



319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
# File 'lib/rsyntaxtree/base_graph.rb', line 319

def finalize_ltr
  @element_list.get_elements.each do |e|
    # Swap position axes
    h = e.horizontal_indent
    v = e.vertical_indent
    e.horizontal_indent = v
    e.vertical_indent = h

    # Restore original content dimensions (text is still horizontal)
    cw = e.content_width
    ch = e.content_height
    e.content_width = ch
    e.content_height = cw
  end

  # Restore original global values
  @global[:h_gap_between_nodes] = @saved_h_gap
  @global[:height_connector] = @saved_height_connector
end

#get_leftmost(id = 1) ⇒ Object



259
260
261
262
263
264
# File 'lib/rsyntaxtree/base_graph.rb', line 259

def get_leftmost(id = 1)
  target = @element_list.get_id(id)
  target_indent = target.horizontal_indent
  children_indent = target.children.map { |c| get_leftmost(c) }
  (children_indent << target_indent).min
end

#get_rightmost(id = 1) ⇒ Object



266
267
268
269
270
271
# File 'lib/rsyntaxtree/base_graph.rb', line 266

def get_rightmost(id = 1)
  target = @element_list.get_id(id)
  target_right_end = target.horizontal_indent + target.content_width
  children_right_end = target.children.map { |c| get_rightmost(c) }
  (children_right_end << target_right_end).max
end

#make_balance(id = 1) ⇒ Object



143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
# File 'lib/rsyntaxtree/base_graph.rb', line 143

def make_balance(id = 1)
  target = @element_list.get_id(id)
  if target.children.empty?
    parent = @element_list.get_id(target.parent)
    accum_array = []
    parent.children.each do |c|
      accum_array << @element_list.get_id(c).width
    end
    max = accum_array.max
    parent.children.each do |c|
      @element_list.get_id(c).width = max
    end
    max
  else
    accum_array = []
    target.children.each do |c|
      accum_array << make_balance(c)
    end
    accum_width = accum_array.max
    max = [accum_width, target.content_width].max
    target.children.each do |c|
      @element_list.get_id(c).width = max
    end
    target.width
  end
end

#node_centeringObject



273
274
275
276
277
278
279
280
281
282
# File 'lib/rsyntaxtree/base_graph.rb', line 273

def node_centering
  node_groups = @element_list.get_elements.group_by(&:parent)
  node_groups.sort_by { |k, _v| -k }.each do |k, v|
    next if k.zero?

    parent = @element_list.get_id(k)
    child_positions = v.map { |child| child.horizontal_indent + child.content_width / 2 }
    parent.horizontal_indent = child_positions.min + (child_positions.max - child_positions.min - parent.content_width) / 2
  end
end

#parse_listObject



339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
# File 'lib/rsyntaxtree/base_graph.rb', line 339

def parse_list
  # Phase 1: swap content dimensions for LTR layout calculation
  prepare_ltr if @direction == "ltr"

  if @element_list.elements.size > 1
    calculate_level
    calculate_width
    make_balance if @symmetrize
    calculate_indent
    node_centering
  end

  top = @element_list.get_id(1)
  diff = top.horizontal_indent
  @element_list.get_elements.each do |e|
    e.horizontal_indent -= diff
  end

  offset_l = (top.horizontal_indent - get_leftmost) + @global[:h_gap_between_nodes]

  @element_list.get_elements.each do |e|
    e.horizontal_indent += offset_l
  end

  calculate_height

  # Phase 2: swap axes and restore content dimensions for LTR
  finalize_ltr if @direction == "ltr"

  draw_elements
  draw_connector
  draw_paths

  # Calculate final bounds
  max_x = 0
  max_y = 0
  @element_list.get_elements.each do |e|
    r = e.horizontal_indent + e.content_width
    b = e.vertical_indent + e.content_height
    max_x = r if r > max_x
    max_y = b if b > max_y
  end
  width = max_x + @global[:h_gap_between_nodes]
  height = max_y
  height = @height if @height > height
  { height: height, width: width }
end

#prepare_ltrObject

LTR layout: two-phase coordinate transformation.

Phase 1 (before layout): swap content dimensions so the layout algorithm uses text height for sibling spreading (→ vertical) and text width for depth spacing (→ horizontal).



289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
# File 'lib/rsyntaxtree/base_graph.rb', line 289

def prepare_ltr
  @element_list.get_elements.each do |e|
    cw = e.content_width
    ch = e.content_height
    e.content_width = ch
    e.content_height = cw
  end

  # Save original global values for restoration in finalize_ltr
  @saved_h_gap = @global[:h_gap_between_nodes]
  @saved_height_connector = @global[:height_connector]

  # In LTR, siblings stack vertically. The TTB h_gap (char_width * 0.8)
  # is disproportionately large relative to the swapped content dimensions.
  # Use height_connector_to_text / 2 (= font_height / 4) for tight
  # vertical packing proportional to the font size.
  @global[:h_gap_between_nodes] = @global[:height_connector_to_text] / 2

  # In LTR, height_connector becomes horizontal depth between levels.
  # After content swap, content_height = original content_width (small),
  # so depth = small_value + height_connector. To maintain proportional
  # depth similar to TTB (where depth = content_height + height_connector),
  # compensate for the content dimension difference.
  metrics = @global[:single_x_metrics]
  content_diff = @global[:single_line_height] - metrics.width
  @global[:height_connector] = @global[:height_connector] + [content_diff, 0].max
end

#subtree_bounds(id = 1) ⇒ Object

Bounding box of the subtree rooted at id, expressed in final drawing coordinates (horizontal_indent / vertical_indent / content_*). Works for both TTB and LTR because it runs after layout (and finalize_ltr) has placed every element. Used to draw whole-subtree region shades.



243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
# File 'lib/rsyntaxtree/base_graph.rb', line 243

def subtree_bounds(id = 1)
  el = @element_list.get_id(id)
  left = el.horizontal_indent
  right = el.horizontal_indent + el.content_width
  top = el.vertical_indent
  bottom = el.vertical_indent + el.content_height
  el.children.each do |c|
    cb = subtree_bounds(c)
    left = cb[:left] if cb[:left] < left
    right = cb[:right] if cb[:right] > right
    top = cb[:top] if cb[:top] < top
    bottom = cb[:bottom] if cb[:bottom] > bottom
  end
  { left: left, right: right, top: top, bottom: bottom }
end