Class: DependencyChart

Inherits:
ChartBase show all
Defined in:
lib/jirametrics/dependency_chart.rb

Defined Under Namespace

Classes: IssueRules, LinkRules

Constant Summary

Constants inherited from ChartBase

ChartBase::LABEL_POSITIONS, ChartBase::OKABE_ITO_PALETTE

Instance Attribute Summary

Attributes inherited from ChartBase

#aggregated_project, #all_boards, #atlassian_document_format, #board_id, #canvas_height, #canvas_width, #data_quality, #date_range, #file_system, #fix_versions, #holiday_dates, #issues, #settings, #time_range, #timezone_offset, #x_axis_title, #y_axis_title

Instance Method Summary collapse

Methods inherited from ChartBase

#aggregated_project?, #before_run, #call_before_run, #canvas, #canvas_responsive?, #chart_format, #collapsible_issues_panel, #color_block, #completed_issues_in_range, #current_board, #cycletime, #cycletime_for_issue, #daily_chart_dataset, #date_annotation, #describe_non_working_days, #description_text, #format_integer, #format_status, #header_text, #holidays, #html_directory, #icon_span, #link_to_issue, #next_id, #non_working_day?, #normalize_annotation_datetime, #not_visible_icon, #not_visible_text, #random_color, #render, #render_axis_title, #render_top_text, #resolve_status, #stagger_label_positions, #status_category_color, #to_human_readable, #working_days_annotation, #wrap_and_render

Constructor Details

#initialize(rules_block) ⇒ DependencyChart

Returns a new instance of DependencyChart.



35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
# File 'lib/jirametrics/dependency_chart.rb', line 35

def initialize rules_block
  super()

  header_text 'Dependencies'
  description_text <<-HTML
    <p>
      These are all the "linked issues" as defined in Jira
    </p>
  HTML

  @rules_block = rules_block

  issue_rules(&default_issue_rules)
  link_rules(&default_link_rules)
end

Instance Method Details

#assemble_dot_graph(visible_issues, link_graph) ⇒ Object



177
178
179
180
181
182
183
184
185
186
187
188
# File 'lib/jirametrics/dependency_chart.rb', line 177

def assemble_dot_graph visible_issues, link_graph
  dot_graph = ['digraph mygraph {', 'rankdir=LR', 'bgcolor="transparent"']

  # Sort the keys so they are proccessed in a deterministic order.
  visible_issues.values.sort_by(&:key_as_i).each do |issue|
    rules = IssueRules.new
    @issue_rules_block.call(issue, rules)
    dot_graph << make_dot_issue(issue: issue, issue_rules: rules)
  end

  dot_graph + link_graph + ['}']
end

#build_dot_graphObject



125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
# File 'lib/jirametrics/dependency_chart.rb', line 125

def build_dot_graph
  issue_links = find_links

  visible_issues = {}
  link_graph = []
  links_to_ignore = []

  issue_links.each do |link|
    next if links_to_ignore.include? link

    link_rules = LinkRules.new
    @link_rules_block.call link, link_rules

    next if link_rules.ignored?
    next if merge_bidirectional_skip?(link, link_rules, issue_links, links_to_ignore)

    link_graph << make_dot_link(issue_link: link, link_rules: link_rules)
    visible_issues[link.origin.key] = link.origin
    visible_issues[link.other_issue.key] = link.other_issue
  end

  return nil if visible_issues.empty?

  assemble_dot_graph(visible_issues, link_graph)
end

#color_for(type:) ⇒ Object

This used to pull colours from chart_base but the migration to CSS colours kept breaking this chart so we moved it here, until we're finished with the rest. TODO: Revisit whether this can also use customizable CSS colours



114
115
116
117
118
119
120
121
122
123
# File 'lib/jirametrics/dependency_chart.rb', line 114

def color_for type:
  @chart_colors = {
    'Story' => '#90EE90',
    'Task' => '#87CEFA',
    'Bug' => '#ffdab9',
    'Defect' => '#ffdab9',
    'Epic' => '#fafad2',
    'Spike' => '#DDA0DD' # light purple
  }[type] ||= random_color
end

#default_issue_rulesObject



230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
# File 'lib/jirametrics/dependency_chart.rb', line 230

def default_issue_rules
  chart = self
  lambda do |issue, rules|
    is_done = issue.done?

    key = issue.key
    key = "<S>#{key} </S> " if is_done
    line2 = +'<BR/>'
    if issue.artificial?
      line2 << '(unknown state)' # Shouldn't happen if we've done a full download but is still possible.
    elsif is_done
      line2 << 'Done'
    else
      started_at = issue.started_stopped_times.first
      if started_at.nil?
        line2 << 'Not started'
      else
        line2 << "Age: #{issue.board.cycletime.age(issue, today: chart.date_range.end)} days"
      end
    end
    rules.label = "<#{key} [#{issue.type}]#{line2}<BR/>#{word_wrap issue.summary}>"
  end
end


254
255
256
257
258
259
260
# File 'lib/jirametrics/dependency_chart.rb', line 254

def default_link_rules
  lambda do |link, rules|
    rules.ignore if link.origin.done? && link.other_issue.done?
    rules.ignore if link.name == 'Cloners'
    rules.merge_bidirectional keep: 'outward'
  end
end

#execute_graphviz(dot_graph) ⇒ Object



190
191
192
193
194
195
196
197
198
199
200
# File 'lib/jirametrics/dependency_chart.rb', line 190

def execute_graphviz dot_graph
  Open3.popen3('dot -Tsvg') do |stdin, stdout, _stderr, _wait_thread|
    stdin.write dot_graph
    stdin.close
    return stdout.read
  end
rescue # rubocop:disable Style/RescueStandardError
  message = 'Unable to generate the dependency chart because graphviz could not be found in the path.'
  file_system.log message, also_write_to_stderr: true
  message
end


72
73
74
75
76
77
78
# File 'lib/jirametrics/dependency_chart.rb', line 72

def find_links
  result = []
  issues.each do |issue|
    result += issue.issue_links
  end
  result
end


169
170
171
172
173
174
175
# File 'lib/jirametrics/dependency_chart.rb', line 169

def find_opposite_link link, issue_links
  issue_links.find do |candidate|
    candidate.name == link.name &&
      candidate.origin.key == link.other_issue.key &&
      candidate.other_issue.key == link.origin.key
  end
end

#issue_rules(&block) ⇒ Object



68
69
70
# File 'lib/jirametrics/dependency_chart.rb', line 68

def issue_rules &block
  @issue_rules_block = block
end


64
65
66
# File 'lib/jirametrics/dependency_chart.rb', line 64

def link_rules &block
  @link_rules_block = block
end

#make_dot_issue(issue:, issue_rules:) ⇒ Object



94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
# File 'lib/jirametrics/dependency_chart.rb', line 94

def make_dot_issue issue:, issue_rules:
  result = +''
  result << issue.key.inspect
  result << '['
  label = issue_rules.label || "#{issue.key}|#{issue.type}"
  label = label.inspect unless label.match?(/^<.+>$/)
  result << "label=#{label}"
  result << ',shape=Mrecord'
  tooltip = "#{issue.key}: #{issue.summary}"
  result << ",tooltip=#{tooltip[0..80].inspect}"
  unless issue_rules.color == :none
    result << %(,style=filled,fillcolor="#{issue_rules.color || color_for(type: issue.type)}")
  end
  result << ']'
  result
end


80
81
82
83
84
85
86
87
88
89
90
91
92
# File 'lib/jirametrics/dependency_chart.rb', line 80

def make_dot_link issue_link:, link_rules:
  result = +''
  result << issue_link.origin.key.inspect
  result << ' -> '
  result << issue_link.other_issue.key.inspect
  result << '['
  result << 'label=' << (link_rules.label || issue_link.label).inspect
  result << ',color=' << (link_rules.line_color || 'gray').inspect
  result << ',fontcolor=' << (link_rules.line_color || 'gray').inspect
  result << ',dir=both' if link_rules.bidirectional_arrows?
  result << '];'
  result
end

#merge_bidirectional_skip?(link, link_rules, issue_links, links_to_ignore) ⇒ Boolean

For a bidirectional-merge link, collapses the pair into one. When this link is the one to keep, its opposite is added to links_to_ignore; returns true when this link should be skipped in favour of its opposite. Returns false (keep this link) when there's no merge or no matching opposite.

Returns:

  • (Boolean)


154
155
156
157
158
159
160
161
162
163
164
165
166
167
# File 'lib/jirametrics/dependency_chart.rb', line 154

def merge_bidirectional_skip? link, link_rules, issue_links, links_to_ignore
  merge_direction = link_rules.get_merge_bidirectional
  return false unless merge_direction

  opposite = find_opposite_link(link, issue_links)
  return false unless opposite

  if merge_direction.to_sym == link.direction
    links_to_ignore << opposite # keep this one, discard the opposite
    false
  else
    true # keep the opposite, skip this one
  end
end

#runObject



51
52
53
54
55
56
57
58
59
60
61
62
# File 'lib/jirametrics/dependency_chart.rb', line 51

def run
  instance_eval(&@rules_block) if @rules_block

  dot_graph = build_dot_graph
  if dot_graph.nil?
    return "<h1 class='foldable'>#{@header_text}</h1>" \
      '<div>No data matched the selected criteria. Nothing to show.</div>'
  end

  svg = execute_graphviz(dot_graph.join("\n"))
  "<h1 class='foldable'>#{@header_text}</h1><div>#{@description_text}#{shrink_svg svg}</div>"
end

#shrink_svg(svg) ⇒ Object



202
203
204
205
206
207
208
209
210
# File 'lib/jirametrics/dependency_chart.rb', line 202

def shrink_svg svg
  scale = 0.8
  svg.sub(/width="(?<width_pt>[\d.]+)pt" height="(?<height_pt>[\d.]+)pt"/) do
    match = Regexp.last_match
    width = match[:width_pt].to_i * scale
    height = match[:height_pt].to_i * scale
    "width=\"#{width.to_i}pt\" height=\"#{height.to_i}pt\""
  end
end

#word_wrap(text, max_width: 50, separator: '<BR/>') ⇒ Object



212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
# File 'lib/jirametrics/dependency_chart.rb', line 212

def word_wrap text, max_width: 50, separator: '<BR/>'
  text.chomp.lines.collect do |line|
    line.chomp!

    # The following characters all cause problems when passed to graphviz
    line.gsub!(/[{<]/, '[')
    line.gsub!(/[}>]/, ']')
    line.gsub!(/\s*&\s*/, ' and ')
    line.delete!('|')

    if line.length > max_width
      line.gsub(/(.{1,#{max_width}})(\s+|$)/, "\\1#{separator}").strip
    else
      line
    end
  end.join(separator)
end